mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
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:
@@ -1,173 +1,167 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { FeedbackSurveyView, isValidResponseInput } from './FeedbackSurveyView.js';
|
||||
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
|
||||
import { TranscriptSharePrompt } from './TranscriptSharePrompt.js';
|
||||
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js';
|
||||
import type { FeedbackSurveyResponse } from './utils.js';
|
||||
import React from 'react'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import {
|
||||
FeedbackSurveyView,
|
||||
isValidResponseInput,
|
||||
} from './FeedbackSurveyView.js'
|
||||
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
|
||||
import { TranscriptSharePrompt } from './TranscriptSharePrompt.js'
|
||||
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'
|
||||
import type { FeedbackSurveyResponse } from './utils.js'
|
||||
|
||||
type Props = {
|
||||
state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
|
||||
lastResponse: FeedbackSurveyResponse | null;
|
||||
handleSelect: (selected: FeedbackSurveyResponse) => void;
|
||||
handleTranscriptSelect?: (selected: TranscriptShareResponse) => void;
|
||||
inputValue: string;
|
||||
setInputValue: (value: string) => void;
|
||||
onRequestFeedback?: () => void;
|
||||
message?: string;
|
||||
};
|
||||
export function FeedbackSurvey(t0) {
|
||||
const $ = _c(16);
|
||||
const {
|
||||
state,
|
||||
lastResponse,
|
||||
handleSelect,
|
||||
handleTranscriptSelect,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
onRequestFeedback,
|
||||
message
|
||||
} = t0;
|
||||
if (state === "closed") {
|
||||
return null;
|
||||
state:
|
||||
| 'closed'
|
||||
| 'open'
|
||||
| 'thanks'
|
||||
| 'transcript_prompt'
|
||||
| 'submitting'
|
||||
| 'submitted'
|
||||
lastResponse: FeedbackSurveyResponse | null
|
||||
handleSelect: (selected: FeedbackSurveyResponse) => void
|
||||
handleTranscriptSelect?: (selected: TranscriptShareResponse) => void
|
||||
inputValue: string
|
||||
setInputValue: (value: string) => void
|
||||
onRequestFeedback?: () => void
|
||||
message?: string
|
||||
}
|
||||
|
||||
export function FeedbackSurvey({
|
||||
state,
|
||||
lastResponse,
|
||||
handleSelect,
|
||||
handleTranscriptSelect,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
onRequestFeedback,
|
||||
message,
|
||||
}: Props): React.ReactNode {
|
||||
if (state === 'closed') {
|
||||
return null
|
||||
}
|
||||
if (state === "thanks") {
|
||||
let t1;
|
||||
if ($[0] !== inputValue || $[1] !== lastResponse || $[2] !== onRequestFeedback || $[3] !== setInputValue) {
|
||||
t1 = <FeedbackSurveyThanks lastResponse={lastResponse} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={onRequestFeedback} />;
|
||||
$[0] = inputValue;
|
||||
$[1] = lastResponse;
|
||||
$[2] = onRequestFeedback;
|
||||
$[3] = setInputValue;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
return t1;
|
||||
|
||||
if (state === 'thanks') {
|
||||
return (
|
||||
<FeedbackSurveyThanks
|
||||
lastResponse={lastResponse}
|
||||
inputValue={inputValue}
|
||||
setInputValue={setInputValue}
|
||||
onRequestFeedback={onRequestFeedback}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (state === "submitted") {
|
||||
let t1;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box marginTop={1}><Text color="success">{"\u2713"} Thanks for sharing your transcript!</Text></Box>;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
|
||||
if (state === 'submitted') {
|
||||
return (
|
||||
<Box marginTop={1}>
|
||||
<Text color="success">
|
||||
{'\u2713'} Thanks for sharing your transcript!
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
if (state === "submitting") {
|
||||
let t1;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box marginTop={1}><Text dimColor={true}>Sharing transcript{"\u2026"}</Text></Box>;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
}
|
||||
return t1;
|
||||
|
||||
if (state === 'submitting') {
|
||||
return (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Sharing transcript{'\u2026'}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
if (state === "transcript_prompt") {
|
||||
|
||||
if (state === 'transcript_prompt') {
|
||||
if (!handleTranscriptSelect) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
if (inputValue && !["1", "2", "3"].includes(inputValue)) {
|
||||
return null;
|
||||
// Hide prompt if user is typing non-response characters
|
||||
if (inputValue && !['1', '2', '3'].includes(inputValue)) {
|
||||
return null
|
||||
}
|
||||
let t1;
|
||||
if ($[7] !== handleTranscriptSelect || $[8] !== inputValue || $[9] !== setInputValue) {
|
||||
t1 = <TranscriptSharePrompt onSelect={handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} />;
|
||||
$[7] = handleTranscriptSelect;
|
||||
$[8] = inputValue;
|
||||
$[9] = setInputValue;
|
||||
$[10] = t1;
|
||||
} else {
|
||||
t1 = $[10];
|
||||
}
|
||||
return t1;
|
||||
return (
|
||||
<TranscriptSharePrompt
|
||||
onSelect={handleTranscriptSelect}
|
||||
inputValue={inputValue}
|
||||
setInputValue={setInputValue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// state === 'open'
|
||||
// Hide the survey if the user is typing anything other than a survey response.
|
||||
// This prevents the survey from showing up when the user is typing a message,
|
||||
// which can result in accidental survey submissions (e.g. "s3cmd").
|
||||
if (inputValue && !isValidResponseInput(inputValue)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t1;
|
||||
if ($[11] !== handleSelect || $[12] !== inputValue || $[13] !== message || $[14] !== setInputValue) {
|
||||
t1 = <FeedbackSurveyView onSelect={handleSelect} inputValue={inputValue} setInputValue={setInputValue} message={message} />;
|
||||
$[11] = handleSelect;
|
||||
$[12] = inputValue;
|
||||
$[13] = message;
|
||||
$[14] = setInputValue;
|
||||
$[15] = t1;
|
||||
} else {
|
||||
t1 = $[15];
|
||||
}
|
||||
return t1;
|
||||
|
||||
return (
|
||||
<FeedbackSurveyView
|
||||
onSelect={handleSelect}
|
||||
inputValue={inputValue}
|
||||
setInputValue={setInputValue}
|
||||
message={message}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type ThanksProps = {
|
||||
lastResponse: FeedbackSurveyResponse | null;
|
||||
inputValue: string;
|
||||
setInputValue: (value: string) => void;
|
||||
onRequestFeedback?: () => void;
|
||||
};
|
||||
const isFollowUpDigit = (char: string): char is '1' => char === '1';
|
||||
function FeedbackSurveyThanks(t0) {
|
||||
const $ = _c(12);
|
||||
const {
|
||||
lastResponse,
|
||||
lastResponse: FeedbackSurveyResponse | null
|
||||
inputValue: string
|
||||
setInputValue: (value: string) => void
|
||||
onRequestFeedback?: () => void
|
||||
}
|
||||
|
||||
const isFollowUpDigit = (char: string): char is '1' => char === '1'
|
||||
|
||||
function FeedbackSurveyThanks({
|
||||
lastResponse,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
onRequestFeedback,
|
||||
}: ThanksProps): React.ReactNode {
|
||||
const showFollowUp = onRequestFeedback && lastResponse === 'good'
|
||||
|
||||
// Listen for "1" keypress to launch /feedback
|
||||
useDebouncedDigitInput({
|
||||
inputValue,
|
||||
setInputValue,
|
||||
onRequestFeedback
|
||||
} = t0;
|
||||
const showFollowUp = onRequestFeedback && lastResponse === "good";
|
||||
const t1 = Boolean(showFollowUp);
|
||||
let t2;
|
||||
if ($[0] !== lastResponse || $[1] !== onRequestFeedback) {
|
||||
t2 = () => {
|
||||
logEvent("tengu_feedback_survey_event", {
|
||||
event_type: "followup_accepted" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
response: lastResponse as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
onRequestFeedback?.();
|
||||
};
|
||||
$[0] = lastResponse;
|
||||
$[1] = onRequestFeedback;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== inputValue || $[4] !== setInputValue || $[5] !== t1 || $[6] !== t2) {
|
||||
t3 = {
|
||||
inputValue,
|
||||
setInputValue,
|
||||
isValidDigit: isFollowUpDigit,
|
||||
enabled: t1,
|
||||
once: true,
|
||||
onDigit: t2
|
||||
};
|
||||
$[3] = inputValue;
|
||||
$[4] = setInputValue;
|
||||
$[5] = t1;
|
||||
$[6] = t2;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
useDebouncedDigitInput(t3);
|
||||
const feedbackCommand = false ? "/issue" : "/feedback";
|
||||
let t4;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Text color="success">Thanks for the feedback!</Text>;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
let t5;
|
||||
if ($[9] !== lastResponse || $[10] !== showFollowUp) {
|
||||
t5 = <Box marginTop={1} flexDirection="column">{t4}{showFollowUp ? <Text dimColor={true}>(Optional) Press [<Text color="ansi:cyan">1</Text>] to tell us what went well {" \xB7 "}{feedbackCommand}</Text> : lastResponse === "bad" ? <Text dimColor={true}>Use /issue to report model behavior issues.</Text> : <Text dimColor={true}>Use {feedbackCommand} to share detailed feedback anytime.</Text>}</Box>;
|
||||
$[9] = lastResponse;
|
||||
$[10] = showFollowUp;
|
||||
$[11] = t5;
|
||||
} else {
|
||||
t5 = $[11];
|
||||
}
|
||||
return t5;
|
||||
isValidDigit: isFollowUpDigit,
|
||||
enabled: Boolean(showFollowUp),
|
||||
once: true,
|
||||
onDigit: () => {
|
||||
logEvent('tengu_feedback_survey_event', {
|
||||
event_type:
|
||||
'followup_accepted' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
response:
|
||||
lastResponse as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
onRequestFeedback?.()
|
||||
},
|
||||
})
|
||||
|
||||
const feedbackCommand =
|
||||
process.env.USER_TYPE === 'ant' ? '/issue' : '/feedback'
|
||||
|
||||
return (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color="success">Thanks for the feedback!</Text>
|
||||
{showFollowUp ? (
|
||||
<Text dimColor>
|
||||
(Optional) Press [<Text color="ansi:cyan">1</Text>] to tell us what
|
||||
went well {' \u00b7 '}
|
||||
{feedbackCommand}
|
||||
</Text>
|
||||
) : lastResponse === 'bad' ? (
|
||||
<Text dimColor>Use /issue to report model behavior issues.</Text>
|
||||
) : (
|
||||
<Text dimColor>
|
||||
Use {feedbackCommand} to share detailed feedback anytime.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,107 +1,72 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js';
|
||||
import type { FeedbackSurveyResponse } from './utils.js';
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'
|
||||
import type { FeedbackSurveyResponse } from './utils.js'
|
||||
|
||||
type Props = {
|
||||
onSelect: (option: FeedbackSurveyResponse) => void;
|
||||
inputValue: string;
|
||||
setInputValue: (value: string) => void;
|
||||
message?: string;
|
||||
};
|
||||
const RESPONSE_INPUTS = ['0', '1', '2', '3'] as const;
|
||||
type ResponseInput = (typeof RESPONSE_INPUTS)[number];
|
||||
onSelect: (option: FeedbackSurveyResponse) => void
|
||||
inputValue: string
|
||||
setInputValue: (value: string) => void
|
||||
message?: string
|
||||
}
|
||||
|
||||
const RESPONSE_INPUTS = ['0', '1', '2', '3'] as const
|
||||
type ResponseInput = (typeof RESPONSE_INPUTS)[number]
|
||||
|
||||
const inputToResponse: Record<ResponseInput, FeedbackSurveyResponse> = {
|
||||
'0': 'dismissed',
|
||||
'1': 'bad',
|
||||
'2': 'fine',
|
||||
'3': 'good'
|
||||
} as const;
|
||||
export const isValidResponseInput = (input: string): input is ResponseInput => (RESPONSE_INPUTS as readonly string[]).includes(input);
|
||||
const DEFAULT_MESSAGE = 'How is Claude doing this session? (optional)';
|
||||
export function FeedbackSurveyView(t0) {
|
||||
const $ = _c(15);
|
||||
const {
|
||||
onSelect,
|
||||
'3': 'good',
|
||||
} as const
|
||||
|
||||
export const isValidResponseInput = (input: string): input is ResponseInput =>
|
||||
(RESPONSE_INPUTS as readonly string[]).includes(input)
|
||||
|
||||
const DEFAULT_MESSAGE = 'How is Claude doing this session? (optional)'
|
||||
|
||||
export function FeedbackSurveyView({
|
||||
onSelect,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
message = DEFAULT_MESSAGE,
|
||||
}: Props): React.ReactNode {
|
||||
useDebouncedDigitInput({
|
||||
inputValue,
|
||||
setInputValue,
|
||||
message: t1
|
||||
} = t0;
|
||||
const message = t1 === undefined ? DEFAULT_MESSAGE : t1;
|
||||
let t2;
|
||||
if ($[0] !== onSelect) {
|
||||
t2 = digit => onSelect(inputToResponse[digit]);
|
||||
$[0] = onSelect;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
let t3;
|
||||
if ($[2] !== inputValue || $[3] !== setInputValue || $[4] !== t2) {
|
||||
t3 = {
|
||||
inputValue,
|
||||
setInputValue,
|
||||
isValidDigit: isValidResponseInput,
|
||||
onDigit: t2
|
||||
};
|
||||
$[2] = inputValue;
|
||||
$[3] = setInputValue;
|
||||
$[4] = t2;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
useDebouncedDigitInput(t3);
|
||||
let t4;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Text color="ansi:cyan">● </Text>;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== message) {
|
||||
t5 = <Box>{t4}<Text bold={true}>{message}</Text></Box>;
|
||||
$[7] = message;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
let t6;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Box width={10}><Text><Text color="ansi:cyan">1</Text>: Bad</Text></Box>;
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
let t7;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Box width={10}><Text><Text color="ansi:cyan">2</Text>: Fine</Text></Box>;
|
||||
$[10] = t7;
|
||||
} else {
|
||||
t7 = $[10];
|
||||
}
|
||||
let t8;
|
||||
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Box width={10}><Text><Text color="ansi:cyan">3</Text>: Good</Text></Box>;
|
||||
$[11] = t8;
|
||||
} else {
|
||||
t8 = $[11];
|
||||
}
|
||||
let t9;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Box marginLeft={2}>{t6}{t7}{t8}<Box><Text><Text color="ansi:cyan">0</Text>: Dismiss</Text></Box></Box>;
|
||||
$[12] = t9;
|
||||
} else {
|
||||
t9 = $[12];
|
||||
}
|
||||
let t10;
|
||||
if ($[13] !== t5) {
|
||||
t10 = <Box flexDirection="column" marginTop={1}>{t5}{t9}</Box>;
|
||||
$[13] = t5;
|
||||
$[14] = t10;
|
||||
} else {
|
||||
t10 = $[14];
|
||||
}
|
||||
return t10;
|
||||
isValidDigit: isValidResponseInput,
|
||||
onDigit: digit => onSelect(inputToResponse[digit]),
|
||||
})
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Box>
|
||||
<Text color="ansi:cyan">● </Text>
|
||||
<Text bold>{message}</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginLeft={2}>
|
||||
<Box width={10}>
|
||||
<Text>
|
||||
<Text color="ansi:cyan">1</Text>: Bad
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={10}>
|
||||
<Text>
|
||||
<Text color="ansi:cyan">2</Text>: Fine
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={10}>
|
||||
<Text>
|
||||
<Text color="ansi:cyan">3</Text>: Good
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>
|
||||
<Text color="ansi:cyan">0</Text>: Dismiss
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,87 +1,74 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js';
|
||||
export type TranscriptShareResponse = 'yes' | 'no' | 'dont_ask_again';
|
||||
import React from 'react'
|
||||
import { BLACK_CIRCLE } from '../../constants/figures.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'
|
||||
|
||||
export type TranscriptShareResponse = 'yes' | 'no' | 'dont_ask_again'
|
||||
|
||||
type Props = {
|
||||
onSelect: (option: TranscriptShareResponse) => void;
|
||||
inputValue: string;
|
||||
setInputValue: (value: string) => void;
|
||||
};
|
||||
const RESPONSE_INPUTS = ['1', '2', '3'] as const;
|
||||
type ResponseInput = (typeof RESPONSE_INPUTS)[number];
|
||||
onSelect: (option: TranscriptShareResponse) => void
|
||||
inputValue: string
|
||||
setInputValue: (value: string) => void
|
||||
}
|
||||
|
||||
const RESPONSE_INPUTS = ['1', '2', '3'] as const
|
||||
type ResponseInput = (typeof RESPONSE_INPUTS)[number]
|
||||
|
||||
const inputToResponse: Record<ResponseInput, TranscriptShareResponse> = {
|
||||
'1': 'yes',
|
||||
'2': 'no',
|
||||
'3': 'dont_ask_again'
|
||||
} as const;
|
||||
const isValidResponseInput = (input: string): input is ResponseInput => (RESPONSE_INPUTS as readonly string[]).includes(input);
|
||||
export function TranscriptSharePrompt(t0) {
|
||||
const $ = _c(11);
|
||||
const {
|
||||
onSelect,
|
||||
'3': 'dont_ask_again',
|
||||
} as const
|
||||
|
||||
const isValidResponseInput = (input: string): input is ResponseInput =>
|
||||
(RESPONSE_INPUTS as readonly string[]).includes(input)
|
||||
|
||||
export function TranscriptSharePrompt({
|
||||
onSelect,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
}: Props): React.ReactNode {
|
||||
useDebouncedDigitInput({
|
||||
inputValue,
|
||||
setInputValue
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== onSelect) {
|
||||
t1 = digit => onSelect(inputToResponse[digit]);
|
||||
$[0] = onSelect;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== inputValue || $[3] !== setInputValue || $[4] !== t1) {
|
||||
t2 = {
|
||||
inputValue,
|
||||
setInputValue,
|
||||
isValidDigit: isValidResponseInput,
|
||||
onDigit: t1
|
||||
};
|
||||
$[2] = inputValue;
|
||||
$[3] = setInputValue;
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
useDebouncedDigitInput(t2);
|
||||
let t3;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box><Text color="ansi:cyan">{BLACK_CIRCLE} </Text><Text bold={true}>Can Anthropic look at your session transcript to help us improve Claude Code?</Text></Box>;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Box marginLeft={2}><Text dimColor={true}>Learn more: https://code.claude.com/docs/en/data-usage#session-quality-surveys</Text></Box>;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Box width={10}><Text><Text color="ansi:cyan">1</Text>: Yes</Text></Box>;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
let t6;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Box width={10}><Text><Text color="ansi:cyan">2</Text>: No</Text></Box>;
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
let t7;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Box flexDirection="column" marginTop={1}>{t3}{t4}<Box marginLeft={2}>{t5}{t6}<Box><Text><Text color="ansi:cyan">3</Text>: Don't ask again</Text></Box></Box></Box>;
|
||||
$[10] = t7;
|
||||
} else {
|
||||
t7 = $[10];
|
||||
}
|
||||
return t7;
|
||||
setInputValue,
|
||||
isValidDigit: isValidResponseInput,
|
||||
onDigit: digit => onSelect(inputToResponse[digit]),
|
||||
})
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Box>
|
||||
<Text color="ansi:cyan">{BLACK_CIRCLE} </Text>
|
||||
<Text bold>
|
||||
Can Anthropic look at your session transcript to help us improve
|
||||
Claude Code?
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginLeft={2}>
|
||||
<Text dimColor>
|
||||
Learn more:
|
||||
https://code.claude.com/docs/en/data-usage#session-quality-surveys
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginLeft={2}>
|
||||
<Box width={10}>
|
||||
<Text>
|
||||
<Text color="ansi:cyan">1</Text>: Yes
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={10}>
|
||||
<Text>
|
||||
<Text color="ansi:cyan">2</Text>: No
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>
|
||||
<Text color="ansi:cyan">3</Text>: Don't ask again
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,32 +1,41 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDynamicConfig } from 'src/hooks/useDynamicConfig.js';
|
||||
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
||||
import { isPolicyAllowed } from '../../services/policyLimits/index.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
||||
import { getLastAssistantMessage } from '../../utils/messages.js';
|
||||
import { getMainLoopModel } from '../../utils/model/model.js';
|
||||
import { getInitialSettings } from '../../utils/settings/settings.js';
|
||||
import { logOTelEvent } from '../../utils/telemetry/events.js';
|
||||
import { submitTranscriptShare, type TranscriptShareTrigger } from './submitTranscriptShare.js';
|
||||
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
|
||||
import { useSurveyState } from './useSurveyState.js';
|
||||
import type { FeedbackSurveyResponse, FeedbackSurveyType } from './utils.js';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useDynamicConfig } from 'src/hooks/useDynamicConfig.js'
|
||||
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { isPolicyAllowed } from '../../services/policyLimits/index.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { getLastAssistantMessage } from '../../utils/messages.js'
|
||||
import { getMainLoopModel } from '../../utils/model/model.js'
|
||||
import { getInitialSettings } from '../../utils/settings/settings.js'
|
||||
import { logOTelEvent } from '../../utils/telemetry/events.js'
|
||||
import {
|
||||
submitTranscriptShare,
|
||||
type TranscriptShareTrigger,
|
||||
} from './submitTranscriptShare.js'
|
||||
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
|
||||
import { useSurveyState } from './useSurveyState.js'
|
||||
import type { FeedbackSurveyResponse, FeedbackSurveyType } from './utils.js'
|
||||
|
||||
type FeedbackSurveyConfig = {
|
||||
minTimeBeforeFeedbackMs: number;
|
||||
minTimeBetweenFeedbackMs: number;
|
||||
minTimeBetweenGlobalFeedbackMs: number;
|
||||
minUserTurnsBeforeFeedback: number;
|
||||
minUserTurnsBetweenFeedback: number;
|
||||
hideThanksAfterMs: number;
|
||||
onForModels: string[];
|
||||
probability: number;
|
||||
};
|
||||
minTimeBeforeFeedbackMs: number
|
||||
minTimeBetweenFeedbackMs: number
|
||||
minTimeBetweenGlobalFeedbackMs: number
|
||||
minUserTurnsBeforeFeedback: number
|
||||
minUserTurnsBetweenFeedback: number
|
||||
hideThanksAfterMs: number
|
||||
onForModels: string[]
|
||||
probability: number
|
||||
}
|
||||
|
||||
type TranscriptAskConfig = {
|
||||
probability: number;
|
||||
};
|
||||
probability: number
|
||||
}
|
||||
|
||||
const DEFAULT_FEEDBACK_SURVEY_CONFIG: FeedbackSurveyConfig = {
|
||||
minTimeBeforeFeedbackMs: 600000,
|
||||
minTimeBetweenFeedbackMs: 3600000,
|
||||
@@ -35,261 +44,381 @@ const DEFAULT_FEEDBACK_SURVEY_CONFIG: FeedbackSurveyConfig = {
|
||||
minUserTurnsBetweenFeedback: 10,
|
||||
hideThanksAfterMs: 3000,
|
||||
onForModels: ['*'],
|
||||
probability: 0.005
|
||||
};
|
||||
probability: 0.005,
|
||||
}
|
||||
|
||||
const DEFAULT_TRANSCRIPT_ASK_CONFIG: TranscriptAskConfig = {
|
||||
probability: 0
|
||||
};
|
||||
export function useFeedbackSurvey(messages: Message[], isLoading: boolean, submitCount: number, surveyType: FeedbackSurveyType = 'session', hasActivePrompt: boolean = false): {
|
||||
state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
|
||||
lastResponse: FeedbackSurveyResponse | null;
|
||||
handleSelect: (selected: FeedbackSurveyResponse) => boolean;
|
||||
handleTranscriptSelect: (selected: TranscriptShareResponse) => void;
|
||||
probability: 0,
|
||||
}
|
||||
|
||||
export function useFeedbackSurvey(
|
||||
messages: Message[],
|
||||
isLoading: boolean,
|
||||
submitCount: number,
|
||||
surveyType: FeedbackSurveyType = 'session',
|
||||
hasActivePrompt: boolean = false,
|
||||
): {
|
||||
state:
|
||||
| 'closed'
|
||||
| 'open'
|
||||
| 'thanks'
|
||||
| 'transcript_prompt'
|
||||
| 'submitting'
|
||||
| 'submitted'
|
||||
lastResponse: FeedbackSurveyResponse | null
|
||||
handleSelect: (selected: FeedbackSurveyResponse) => boolean
|
||||
handleTranscriptSelect: (selected: TranscriptShareResponse) => void
|
||||
} {
|
||||
const lastAssistantMessageIdRef = useRef('unknown');
|
||||
lastAssistantMessageIdRef.current = getLastAssistantMessage(messages)?.message?.id || 'unknown';
|
||||
const lastAssistantMessageIdRef = useRef('unknown')
|
||||
lastAssistantMessageIdRef.current =
|
||||
getLastAssistantMessage(messages)?.message?.id || 'unknown'
|
||||
const [feedbackSurvey, setFeedbackSurvey] = useState<{
|
||||
timeLastShown: number | null;
|
||||
submitCountAtLastAppearance: number | null;
|
||||
}>(() => ({
|
||||
timeLastShown: null,
|
||||
submitCountAtLastAppearance: null
|
||||
}));
|
||||
const config = useDynamicConfig<FeedbackSurveyConfig>('tengu_feedback_survey_config', DEFAULT_FEEDBACK_SURVEY_CONFIG);
|
||||
const badTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>('tengu_bad_survey_transcript_ask_config', DEFAULT_TRANSCRIPT_ASK_CONFIG);
|
||||
const goodTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>('tengu_good_survey_transcript_ask_config', DEFAULT_TRANSCRIPT_ASK_CONFIG);
|
||||
const settingsRate = getInitialSettings().feedbackSurveyRate;
|
||||
const sessionStartTime = useRef(Date.now());
|
||||
const submitCountAtSessionStart = useRef(submitCount);
|
||||
const submitCountRef = useRef(submitCount);
|
||||
submitCountRef.current = submitCount;
|
||||
const messagesRef = useRef(messages);
|
||||
messagesRef.current = messages;
|
||||
timeLastShown: number | null
|
||||
submitCountAtLastAppearance: number | null
|
||||
}>(() => ({ timeLastShown: null, submitCountAtLastAppearance: null }))
|
||||
const config = useDynamicConfig<FeedbackSurveyConfig>(
|
||||
'tengu_feedback_survey_config',
|
||||
DEFAULT_FEEDBACK_SURVEY_CONFIG,
|
||||
)
|
||||
const badTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>(
|
||||
'tengu_bad_survey_transcript_ask_config',
|
||||
DEFAULT_TRANSCRIPT_ASK_CONFIG,
|
||||
)
|
||||
const goodTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>(
|
||||
'tengu_good_survey_transcript_ask_config',
|
||||
DEFAULT_TRANSCRIPT_ASK_CONFIG,
|
||||
)
|
||||
const settingsRate = getInitialSettings().feedbackSurveyRate
|
||||
const sessionStartTime = useRef(Date.now())
|
||||
const submitCountAtSessionStart = useRef(submitCount)
|
||||
const submitCountRef = useRef(submitCount)
|
||||
submitCountRef.current = submitCount
|
||||
const messagesRef = useRef(messages)
|
||||
messagesRef.current = messages
|
||||
// Probability gate: roll once when eligibility conditions are met, not on every
|
||||
// useMemo re-evaluation. Without this, each dependency change (submitCount,
|
||||
// isLoading toggle, etc.) re-rolls Math.random(), making the survey almost
|
||||
// certain to appear after enough renders.
|
||||
const probabilityPassedRef = useRef(false);
|
||||
const lastEligibleSubmitCountRef = useRef<number | null>(null);
|
||||
const updateLastShownTime = useCallback((timestamp: number, submitCountValue: number) => {
|
||||
setFeedbackSurvey(prev => {
|
||||
if (prev.timeLastShown === timestamp && prev.submitCountAtLastAppearance === submitCountValue) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
timeLastShown: timestamp,
|
||||
submitCountAtLastAppearance: submitCountValue
|
||||
};
|
||||
});
|
||||
// Persist cross-session pacing state (previously done by onChangeAppState observer)
|
||||
if (getGlobalConfig().feedbackSurveyState?.lastShownTime !== timestamp) {
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
feedbackSurveyState: {
|
||||
lastShownTime: timestamp
|
||||
const probabilityPassedRef = useRef(false)
|
||||
const lastEligibleSubmitCountRef = useRef<number | null>(null)
|
||||
|
||||
const updateLastShownTime = useCallback(
|
||||
(timestamp: number, submitCountValue: number) => {
|
||||
setFeedbackSurvey(prev => {
|
||||
if (
|
||||
prev.timeLastShown === timestamp &&
|
||||
prev.submitCountAtLastAppearance === submitCountValue
|
||||
) {
|
||||
return prev
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
const onOpen = useCallback((appearanceId: string) => {
|
||||
updateLastShownTime(Date.now(), submitCountRef.current);
|
||||
logEvent('tengu_feedback_survey_event', {
|
||||
event_type: 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
void logOTelEvent('feedback_survey', {
|
||||
event_type: 'appeared',
|
||||
appearance_id: appearanceId,
|
||||
survey_type: surveyType
|
||||
});
|
||||
}, [updateLastShownTime, surveyType]);
|
||||
const onSelect = useCallback((appearanceId_0: string, selected: FeedbackSurveyResponse) => {
|
||||
updateLastShownTime(Date.now(), submitCountRef.current);
|
||||
logEvent('tengu_feedback_survey_event', {
|
||||
event_type: 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
void logOTelEvent('feedback_survey', {
|
||||
event_type: 'responded',
|
||||
appearance_id: appearanceId_0,
|
||||
response: selected,
|
||||
survey_type: surveyType
|
||||
});
|
||||
}, [updateLastShownTime, surveyType]);
|
||||
const shouldShowTranscriptPrompt = useCallback((selected_0: FeedbackSurveyResponse) => {
|
||||
// Only bad and good ratings trigger the transcript ask
|
||||
if (selected_0 !== 'bad' && selected_0 !== 'good') {
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
timeLastShown: timestamp,
|
||||
submitCountAtLastAppearance: submitCountValue,
|
||||
}
|
||||
})
|
||||
// Persist cross-session pacing state (previously done by onChangeAppState observer)
|
||||
if (getGlobalConfig().feedbackSurveyState?.lastShownTime !== timestamp) {
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
feedbackSurveyState: {
|
||||
lastShownTime: timestamp,
|
||||
},
|
||||
}))
|
||||
}
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
// Don't show if user previously chose "Don't ask again"
|
||||
if (getGlobalConfig().transcriptShareDismissed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't show if product feedback is blocked by org policy (ZDR)
|
||||
if (!isPolicyAllowed('allow_product_feedback')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Probability gate from GrowthBook config (separate per rating)
|
||||
const probability = selected_0 === 'bad' ? badTranscriptAskConfig.probability : goodTranscriptAskConfig.probability;
|
||||
return Math.random() <= probability;
|
||||
}, [badTranscriptAskConfig.probability, goodTranscriptAskConfig.probability]);
|
||||
const onTranscriptPromptShown = useCallback((appearanceId_1: string, surveyResponse: FeedbackSurveyResponse) => {
|
||||
const trigger: TranscriptShareTrigger = surveyResponse === 'good' ? 'good_feedback_survey' : 'bad_feedback_survey';
|
||||
logEvent('tengu_feedback_survey_event', {
|
||||
event_type: 'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id: appearanceId_1 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
trigger: trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
void logOTelEvent('feedback_survey', {
|
||||
event_type: 'transcript_prompt_appeared',
|
||||
appearance_id: appearanceId_1,
|
||||
survey_type: surveyType
|
||||
});
|
||||
}, [surveyType]);
|
||||
const onTranscriptSelect = useCallback(async (appearanceId_2: string, selected_1: TranscriptShareResponse, surveyResponse_0: FeedbackSurveyResponse | null): Promise<boolean> => {
|
||||
const trigger_0: TranscriptShareTrigger = surveyResponse_0 === 'good' ? 'good_feedback_survey' : 'bad_feedback_survey';
|
||||
logEvent('tengu_feedback_survey_event', {
|
||||
event_type: `transcript_share_${selected_1}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
trigger: trigger_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
if (selected_1 === 'dont_ask_again') {
|
||||
saveGlobalConfig(current_0 => ({
|
||||
...current_0,
|
||||
transcriptShareDismissed: true
|
||||
}));
|
||||
}
|
||||
if (selected_1 === 'yes') {
|
||||
const result = await submitTranscriptShare(messagesRef.current, trigger_0, appearanceId_2);
|
||||
const onOpen = useCallback(
|
||||
(appearanceId: string) => {
|
||||
updateLastShownTime(Date.now(), submitCountRef.current)
|
||||
logEvent('tengu_feedback_survey_event', {
|
||||
event_type: (result.success ? 'transcript_share_submitted' : 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
trigger: trigger_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
return result.success;
|
||||
}
|
||||
return false;
|
||||
}, [surveyType]);
|
||||
const {
|
||||
state,
|
||||
lastResponse,
|
||||
open,
|
||||
handleSelect,
|
||||
handleTranscriptSelect
|
||||
} = useSurveyState({
|
||||
hideThanksAfterMs: config.hideThanksAfterMs,
|
||||
onOpen,
|
||||
onSelect,
|
||||
shouldShowTranscriptPrompt,
|
||||
onTranscriptPromptShown,
|
||||
onTranscriptSelect
|
||||
});
|
||||
const currentModel = getMainLoopModel();
|
||||
event_type:
|
||||
'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id:
|
||||
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
last_assistant_message_id:
|
||||
lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
survey_type:
|
||||
surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
void logOTelEvent('feedback_survey', {
|
||||
event_type: 'appeared',
|
||||
appearance_id: appearanceId,
|
||||
survey_type: surveyType,
|
||||
})
|
||||
},
|
||||
[updateLastShownTime, surveyType],
|
||||
)
|
||||
|
||||
const onSelect = useCallback(
|
||||
(appearanceId: string, selected: FeedbackSurveyResponse) => {
|
||||
updateLastShownTime(Date.now(), submitCountRef.current)
|
||||
logEvent('tengu_feedback_survey_event', {
|
||||
event_type:
|
||||
'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id:
|
||||
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
response:
|
||||
selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
last_assistant_message_id:
|
||||
lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
survey_type:
|
||||
surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
void logOTelEvent('feedback_survey', {
|
||||
event_type: 'responded',
|
||||
appearance_id: appearanceId,
|
||||
response: selected,
|
||||
survey_type: surveyType,
|
||||
})
|
||||
},
|
||||
[updateLastShownTime, surveyType],
|
||||
)
|
||||
|
||||
const shouldShowTranscriptPrompt = useCallback(
|
||||
(selected: FeedbackSurveyResponse) => {
|
||||
// Only bad and good ratings trigger the transcript ask
|
||||
if (selected !== 'bad' && selected !== 'good') {
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't show if user previously chose "Don't ask again"
|
||||
if (getGlobalConfig().transcriptShareDismissed) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't show if product feedback is blocked by org policy (ZDR)
|
||||
if (!isPolicyAllowed('allow_product_feedback')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Probability gate from GrowthBook config (separate per rating)
|
||||
const probability =
|
||||
selected === 'bad'
|
||||
? badTranscriptAskConfig.probability
|
||||
: goodTranscriptAskConfig.probability
|
||||
return Math.random() <= probability
|
||||
},
|
||||
[badTranscriptAskConfig.probability, goodTranscriptAskConfig.probability],
|
||||
)
|
||||
|
||||
const onTranscriptPromptShown = useCallback(
|
||||
(appearanceId: string, surveyResponse: FeedbackSurveyResponse) => {
|
||||
const trigger: TranscriptShareTrigger =
|
||||
surveyResponse === 'good'
|
||||
? 'good_feedback_survey'
|
||||
: 'bad_feedback_survey'
|
||||
logEvent('tengu_feedback_survey_event', {
|
||||
event_type:
|
||||
'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id:
|
||||
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
last_assistant_message_id:
|
||||
lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
survey_type:
|
||||
surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
trigger:
|
||||
trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
void logOTelEvent('feedback_survey', {
|
||||
event_type: 'transcript_prompt_appeared',
|
||||
appearance_id: appearanceId,
|
||||
survey_type: surveyType,
|
||||
})
|
||||
},
|
||||
[surveyType],
|
||||
)
|
||||
|
||||
const onTranscriptSelect = useCallback(
|
||||
async (
|
||||
appearanceId: string,
|
||||
selected: TranscriptShareResponse,
|
||||
surveyResponse: FeedbackSurveyResponse | null,
|
||||
): Promise<boolean> => {
|
||||
const trigger: TranscriptShareTrigger =
|
||||
surveyResponse === 'good'
|
||||
? 'good_feedback_survey'
|
||||
: 'bad_feedback_survey'
|
||||
|
||||
logEvent('tengu_feedback_survey_event', {
|
||||
event_type:
|
||||
`transcript_share_${selected}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id:
|
||||
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
last_assistant_message_id:
|
||||
lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
survey_type:
|
||||
surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
trigger:
|
||||
trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
|
||||
if (selected === 'dont_ask_again') {
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
transcriptShareDismissed: true,
|
||||
}))
|
||||
}
|
||||
|
||||
if (selected === 'yes') {
|
||||
const result = await submitTranscriptShare(
|
||||
messagesRef.current,
|
||||
trigger,
|
||||
appearanceId,
|
||||
)
|
||||
logEvent('tengu_feedback_survey_event', {
|
||||
event_type: (result.success
|
||||
? 'transcript_share_submitted'
|
||||
: 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id:
|
||||
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
trigger:
|
||||
trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
return result.success
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
[surveyType],
|
||||
)
|
||||
|
||||
const { state, lastResponse, open, handleSelect, handleTranscriptSelect } =
|
||||
useSurveyState({
|
||||
hideThanksAfterMs: config.hideThanksAfterMs,
|
||||
onOpen,
|
||||
onSelect,
|
||||
shouldShowTranscriptPrompt,
|
||||
onTranscriptPromptShown,
|
||||
onTranscriptSelect,
|
||||
})
|
||||
|
||||
const currentModel = getMainLoopModel()
|
||||
const isModelAllowed = useMemo(() => {
|
||||
if (config.onForModels.length === 0) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (config.onForModels.includes('*')) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
return config.onForModels.includes(currentModel);
|
||||
}, [config.onForModels, currentModel]);
|
||||
return config.onForModels.includes(currentModel)
|
||||
}, [config.onForModels, currentModel])
|
||||
|
||||
const shouldOpen = useMemo(() => {
|
||||
if (state !== 'closed') {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't show survey when permission or ask question prompts are visible
|
||||
if (hasActivePrompt) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
// Force display for testing
|
||||
if (process.env.CLAUDE_FORCE_DISPLAY_SURVEY && !feedbackSurvey.timeLastShown) {
|
||||
return true;
|
||||
if (
|
||||
process.env.CLAUDE_FORCE_DISPLAY_SURVEY &&
|
||||
!feedbackSurvey.timeLastShown
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!isModelAllowed) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
if (isFeedbackSurveyDisabled()) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if product feedback is allowed by org policy
|
||||
if (!isPolicyAllowed('allow_product_feedback')) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
// Check session-local pacing
|
||||
if (feedbackSurvey.timeLastShown) {
|
||||
// Check time elapsed since last appearance in this session
|
||||
const timeSinceLastShown = Date.now() - feedbackSurvey.timeLastShown;
|
||||
const timeSinceLastShown = Date.now() - feedbackSurvey.timeLastShown
|
||||
if (timeSinceLastShown < config.minTimeBetweenFeedbackMs) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
// Check user turn requirement for subsequent appearances
|
||||
if (feedbackSurvey.submitCountAtLastAppearance !== null && submitCount < feedbackSurvey.submitCountAtLastAppearance + config.minUserTurnsBetweenFeedback) {
|
||||
return false;
|
||||
if (
|
||||
feedbackSurvey.submitCountAtLastAppearance !== null &&
|
||||
submitCount <
|
||||
feedbackSurvey.submitCountAtLastAppearance +
|
||||
config.minUserTurnsBetweenFeedback
|
||||
) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// First appearance in this session
|
||||
const timeSinceSessionStart = Date.now() - sessionStartTime.current;
|
||||
const timeSinceSessionStart = Date.now() - sessionStartTime.current
|
||||
if (timeSinceSessionStart < config.minTimeBeforeFeedbackMs) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (submitCount < submitCountAtSessionStart.current + config.minUserTurnsBeforeFeedback) {
|
||||
return false;
|
||||
if (
|
||||
submitCount <
|
||||
submitCountAtSessionStart.current + config.minUserTurnsBeforeFeedback
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Probability check: roll once per eligibility window to avoid re-rolling
|
||||
// on every useMemo re-evaluation (which would make triggering near-certain).
|
||||
if (lastEligibleSubmitCountRef.current !== submitCount) {
|
||||
lastEligibleSubmitCountRef.current = submitCount;
|
||||
probabilityPassedRef.current = Math.random() <= (settingsRate ?? config.probability);
|
||||
lastEligibleSubmitCountRef.current = submitCount
|
||||
probabilityPassedRef.current =
|
||||
Math.random() <= (settingsRate ?? config.probability)
|
||||
}
|
||||
if (!probabilityPassedRef.current) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
// Check global pacing (across all sessions)
|
||||
// Leave this till last because it reads from the filesystem which is expensive.
|
||||
const globalFeedbackState = getGlobalConfig().feedbackSurveyState;
|
||||
const globalFeedbackState = getGlobalConfig().feedbackSurveyState
|
||||
if (globalFeedbackState?.lastShownTime) {
|
||||
const timeSinceGlobalLastShown = Date.now() - globalFeedbackState.lastShownTime;
|
||||
const timeSinceGlobalLastShown =
|
||||
Date.now() - globalFeedbackState.lastShownTime
|
||||
if (timeSinceGlobalLastShown < config.minTimeBetweenGlobalFeedbackMs) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}, [state, isLoading, hasActivePrompt, isModelAllowed, feedbackSurvey.timeLastShown, feedbackSurvey.submitCountAtLastAppearance, submitCount, config.minTimeBetweenFeedbackMs, config.minTimeBetweenGlobalFeedbackMs, config.minUserTurnsBetweenFeedback, config.minTimeBeforeFeedbackMs, config.minUserTurnsBeforeFeedback, config.probability, settingsRate]);
|
||||
|
||||
return true
|
||||
}, [
|
||||
state,
|
||||
isLoading,
|
||||
hasActivePrompt,
|
||||
isModelAllowed,
|
||||
feedbackSurvey.timeLastShown,
|
||||
feedbackSurvey.submitCountAtLastAppearance,
|
||||
submitCount,
|
||||
config.minTimeBetweenFeedbackMs,
|
||||
config.minTimeBetweenGlobalFeedbackMs,
|
||||
config.minUserTurnsBetweenFeedback,
|
||||
config.minTimeBeforeFeedbackMs,
|
||||
config.minUserTurnsBeforeFeedback,
|
||||
config.probability,
|
||||
settingsRate,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldOpen) {
|
||||
open();
|
||||
open()
|
||||
}
|
||||
}, [shouldOpen, open]);
|
||||
return {
|
||||
state,
|
||||
lastResponse,
|
||||
handleSelect,
|
||||
handleTranscriptSelect
|
||||
};
|
||||
}, [shouldOpen, open])
|
||||
|
||||
return { state, lastResponse, handleSelect, handleTranscriptSelect }
|
||||
}
|
||||
|
||||
@@ -1,212 +1,283 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js';
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
||||
import { isAutoMemoryEnabled } from '../../memdir/paths.js';
|
||||
import { isPolicyAllowed } from '../../services/policyLimits/index.js';
|
||||
import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
||||
import { isAutoManagedMemoryFile } from '../../utils/memoryFileDetection.js';
|
||||
import { extractTextContent, getLastAssistantMessage } from '../../utils/messages.js';
|
||||
import { logOTelEvent } from '../../utils/telemetry/events.js';
|
||||
import { submitTranscriptShare } from './submitTranscriptShare.js';
|
||||
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
|
||||
import { useSurveyState } from './useSurveyState.js';
|
||||
import type { FeedbackSurveyResponse } from './utils.js';
|
||||
const HIDE_THANKS_AFTER_MS = 3000;
|
||||
const MEMORY_SURVEY_GATE = 'tengu_dunwich_bell';
|
||||
const MEMORY_SURVEY_EVENT = 'tengu_memory_survey_event';
|
||||
const SURVEY_PROBABILITY = 0.2;
|
||||
const TRANSCRIPT_SHARE_TRIGGER = 'memory_survey';
|
||||
const MEMORY_WORD_RE = /\bmemor(?:y|ies)\b/i;
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { isAutoMemoryEnabled } from '../../memdir/paths.js'
|
||||
import { isPolicyAllowed } from '../../services/policyLimits/index.js'
|
||||
import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { isAutoManagedMemoryFile } from '../../utils/memoryFileDetection.js'
|
||||
import {
|
||||
extractTextContent,
|
||||
getLastAssistantMessage,
|
||||
} from '../../utils/messages.js'
|
||||
import { logOTelEvent } from '../../utils/telemetry/events.js'
|
||||
import { submitTranscriptShare } from './submitTranscriptShare.js'
|
||||
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
|
||||
import { useSurveyState } from './useSurveyState.js'
|
||||
import type { FeedbackSurveyResponse } from './utils.js'
|
||||
|
||||
const HIDE_THANKS_AFTER_MS = 3000
|
||||
const MEMORY_SURVEY_GATE = 'tengu_dunwich_bell'
|
||||
const MEMORY_SURVEY_EVENT = 'tengu_memory_survey_event'
|
||||
const SURVEY_PROBABILITY = 0.2
|
||||
const TRANSCRIPT_SHARE_TRIGGER = 'memory_survey'
|
||||
|
||||
const MEMORY_WORD_RE = /\bmemor(?:y|ies)\b/i
|
||||
|
||||
function hasMemoryFileRead(messages: Message[]): boolean {
|
||||
for (const message of messages) {
|
||||
if (message.type !== 'assistant') {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
const content = message.message.content;
|
||||
const content = message.message.content
|
||||
if (!Array.isArray(content)) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
for (const block of content) {
|
||||
if (block.type !== 'tool_use' || block.name !== FILE_READ_TOOL_NAME) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
const input = block.input as {
|
||||
file_path?: unknown;
|
||||
};
|
||||
if (typeof input.file_path === 'string' && isAutoManagedMemoryFile(input.file_path)) {
|
||||
return true;
|
||||
const input = block.input as { file_path?: unknown }
|
||||
if (
|
||||
typeof input.file_path === 'string' &&
|
||||
isAutoManagedMemoryFile(input.file_path)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
export function useMemorySurvey(messages: Message[], isLoading: boolean, hasActivePrompt = false, {
|
||||
enabled = true
|
||||
}: {
|
||||
enabled?: boolean;
|
||||
} = {}): {
|
||||
state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
|
||||
lastResponse: FeedbackSurveyResponse | null;
|
||||
handleSelect: (selected: FeedbackSurveyResponse) => void;
|
||||
handleTranscriptSelect: (selected: TranscriptShareResponse) => void;
|
||||
|
||||
export function useMemorySurvey(
|
||||
messages: Message[],
|
||||
isLoading: boolean,
|
||||
hasActivePrompt = false,
|
||||
{ enabled = true }: { enabled?: boolean } = {},
|
||||
): {
|
||||
state:
|
||||
| 'closed'
|
||||
| 'open'
|
||||
| 'thanks'
|
||||
| 'transcript_prompt'
|
||||
| 'submitting'
|
||||
| 'submitted'
|
||||
lastResponse: FeedbackSurveyResponse | null
|
||||
handleSelect: (selected: FeedbackSurveyResponse) => void
|
||||
handleTranscriptSelect: (selected: TranscriptShareResponse) => void
|
||||
} {
|
||||
// Track assistant message UUIDs that were already evaluated so we don't
|
||||
// re-roll probability on re-renders or re-scan messages for the same turn.
|
||||
const seenAssistantUuids = useRef<Set<string>>(new Set());
|
||||
const seenAssistantUuids = useRef<Set<string>>(new Set())
|
||||
// Once a memory file read is observed it stays true for the session —
|
||||
// skip the O(n) scan on subsequent turns.
|
||||
const memoryReadSeen = useRef(false);
|
||||
const messagesRef = useRef(messages);
|
||||
messagesRef.current = messages;
|
||||
const memoryReadSeen = useRef(false)
|
||||
const messagesRef = useRef(messages)
|
||||
messagesRef.current = messages
|
||||
|
||||
const onOpen = useCallback((appearanceId: string) => {
|
||||
logEvent(MEMORY_SURVEY_EVENT, {
|
||||
event_type: 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
event_type:
|
||||
'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id:
|
||||
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
void logOTelEvent('feedback_survey', {
|
||||
event_type: 'appeared',
|
||||
appearance_id: appearanceId,
|
||||
survey_type: 'memory'
|
||||
});
|
||||
}, []);
|
||||
const onSelect = useCallback((appearanceId_0: string, selected: FeedbackSurveyResponse) => {
|
||||
survey_type: 'memory',
|
||||
})
|
||||
}, [])
|
||||
|
||||
const onSelect = useCallback(
|
||||
(appearanceId: string, selected: FeedbackSurveyResponse) => {
|
||||
logEvent(MEMORY_SURVEY_EVENT, {
|
||||
event_type:
|
||||
'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id:
|
||||
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
response:
|
||||
selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
void logOTelEvent('feedback_survey', {
|
||||
event_type: 'responded',
|
||||
appearance_id: appearanceId,
|
||||
response: selected,
|
||||
survey_type: 'memory',
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const shouldShowTranscriptPrompt = useCallback(
|
||||
(selected: FeedbackSurveyResponse) => {
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return false
|
||||
}
|
||||
if (selected !== 'bad' && selected !== 'good') {
|
||||
return false
|
||||
}
|
||||
if (getGlobalConfig().transcriptShareDismissed) {
|
||||
return false
|
||||
}
|
||||
if (!isPolicyAllowed('allow_product_feedback')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const onTranscriptPromptShown = useCallback((appearanceId: string) => {
|
||||
logEvent(MEMORY_SURVEY_EVENT, {
|
||||
event_type: 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
void logOTelEvent('feedback_survey', {
|
||||
event_type: 'responded',
|
||||
appearance_id: appearanceId_0,
|
||||
response: selected,
|
||||
survey_type: 'memory'
|
||||
});
|
||||
}, []);
|
||||
const shouldShowTranscriptPrompt = useCallback((selected_0: FeedbackSurveyResponse) => {
|
||||
if ((process.env.USER_TYPE) !== 'ant') {
|
||||
return false;
|
||||
}
|
||||
if (selected_0 !== 'bad' && selected_0 !== 'good') {
|
||||
return false;
|
||||
}
|
||||
if (getGlobalConfig().transcriptShareDismissed) {
|
||||
return false;
|
||||
}
|
||||
if (!isPolicyAllowed('allow_product_feedback')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, []);
|
||||
const onTranscriptPromptShown = useCallback((appearanceId_1: string) => {
|
||||
logEvent(MEMORY_SURVEY_EVENT, {
|
||||
event_type: 'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id: appearanceId_1 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
event_type:
|
||||
'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id:
|
||||
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
trigger:
|
||||
TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
void logOTelEvent('feedback_survey', {
|
||||
event_type: 'transcript_prompt_appeared',
|
||||
appearance_id: appearanceId_1,
|
||||
survey_type: 'memory'
|
||||
});
|
||||
}, []);
|
||||
const onTranscriptSelect = useCallback(async (appearanceId_2: string, selected_1: TranscriptShareResponse): Promise<boolean> => {
|
||||
logEvent(MEMORY_SURVEY_EVENT, {
|
||||
event_type: `transcript_share_${selected_1}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
if (selected_1 === 'dont_ask_again') {
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
transcriptShareDismissed: true
|
||||
}));
|
||||
}
|
||||
if (selected_1 === 'yes') {
|
||||
const result = await submitTranscriptShare(messagesRef.current, TRANSCRIPT_SHARE_TRIGGER, appearanceId_2);
|
||||
appearance_id: appearanceId,
|
||||
survey_type: 'memory',
|
||||
})
|
||||
}, [])
|
||||
|
||||
const onTranscriptSelect = useCallback(
|
||||
async (
|
||||
appearanceId: string,
|
||||
selected: TranscriptShareResponse,
|
||||
): Promise<boolean> => {
|
||||
logEvent(MEMORY_SURVEY_EVENT, {
|
||||
event_type: (result.success ? 'transcript_share_submitted' : 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
return result.success;
|
||||
}
|
||||
return false;
|
||||
}, []);
|
||||
const {
|
||||
state,
|
||||
lastResponse,
|
||||
open,
|
||||
handleSelect,
|
||||
handleTranscriptSelect
|
||||
} = useSurveyState({
|
||||
hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
|
||||
onOpen,
|
||||
onSelect,
|
||||
shouldShowTranscriptPrompt,
|
||||
onTranscriptPromptShown,
|
||||
onTranscriptSelect
|
||||
});
|
||||
const lastAssistant = useMemo(() => getLastAssistantMessage(messages), [messages]);
|
||||
event_type:
|
||||
`transcript_share_${selected}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id:
|
||||
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
trigger:
|
||||
TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
|
||||
if (selected === 'dont_ask_again') {
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
transcriptShareDismissed: true,
|
||||
}))
|
||||
}
|
||||
|
||||
if (selected === 'yes') {
|
||||
const result = await submitTranscriptShare(
|
||||
messagesRef.current,
|
||||
TRANSCRIPT_SHARE_TRIGGER,
|
||||
appearanceId,
|
||||
)
|
||||
logEvent(MEMORY_SURVEY_EVENT, {
|
||||
event_type: (result.success
|
||||
? 'transcript_share_submitted'
|
||||
: 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id:
|
||||
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
trigger:
|
||||
TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
return result.success
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const { state, lastResponse, open, handleSelect, handleTranscriptSelect } =
|
||||
useSurveyState({
|
||||
hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
|
||||
onOpen,
|
||||
onSelect,
|
||||
shouldShowTranscriptPrompt,
|
||||
onTranscriptPromptShown,
|
||||
onTranscriptSelect,
|
||||
})
|
||||
|
||||
const lastAssistant = useMemo(
|
||||
() => getLastAssistantMessage(messages),
|
||||
[messages],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
if (!enabled) return
|
||||
|
||||
// /clear resets messages but REPL stays mounted — reset refs so a memory
|
||||
// read from the previous conversation doesn't leak into the new one.
|
||||
if (messages.length === 0) {
|
||||
memoryReadSeen.current = false;
|
||||
seenAssistantUuids.current.clear();
|
||||
return;
|
||||
memoryReadSeen.current = false
|
||||
seenAssistantUuids.current.clear()
|
||||
return
|
||||
}
|
||||
|
||||
if (state !== 'closed' || isLoading || hasActivePrompt) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// 3P default: survey off (no GrowthBook on Bedrock/Vertex/Foundry).
|
||||
if (!getFeatureValue_CACHED_MAY_BE_STALE(MEMORY_SURVEY_GATE, false)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (!isAutoMemoryEnabled()) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (isFeedbackSurveyDisabled()) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (!isPolicyAllowed('allow_product_feedback')) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (!lastAssistant || seenAssistantUuids.current.has(lastAssistant.uuid)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const text = extractTextContent(Array.isArray(lastAssistant.message.content) ? lastAssistant.message.content : [], ' ');
|
||||
|
||||
const text = extractTextContent(lastAssistant.message.content, ' ')
|
||||
if (!MEMORY_WORD_RE.test(text)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// Mark as evaluated before the memory-read scan so a turn that mentions
|
||||
// "memory" but has no memory read doesn't trigger repeated O(n) scans
|
||||
// on subsequent renders with the same last assistant message.
|
||||
seenAssistantUuids.current.add(lastAssistant.uuid);
|
||||
seenAssistantUuids.current.add(lastAssistant.uuid)
|
||||
|
||||
if (!memoryReadSeen.current) {
|
||||
memoryReadSeen.current = hasMemoryFileRead(messages);
|
||||
memoryReadSeen.current = hasMemoryFileRead(messages)
|
||||
}
|
||||
if (!memoryReadSeen.current) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (Math.random() < SURVEY_PROBABILITY) {
|
||||
open();
|
||||
open()
|
||||
}
|
||||
}, [enabled, state, isLoading, hasActivePrompt, lastAssistant, messages, open]);
|
||||
return {
|
||||
}, [
|
||||
enabled,
|
||||
state,
|
||||
lastResponse,
|
||||
handleSelect,
|
||||
handleTranscriptSelect
|
||||
};
|
||||
isLoading,
|
||||
hasActivePrompt,
|
||||
lastAssistant,
|
||||
messages,
|
||||
open,
|
||||
])
|
||||
|
||||
return { state, lastResponse, handleSelect, handleTranscriptSelect }
|
||||
}
|
||||
|
||||
@@ -1,205 +1,195 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js';
|
||||
import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
||||
import { shouldUseSessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
||||
import { isCompactBoundaryMessage } from '../../utils/messages.js';
|
||||
import { logOTelEvent } from '../../utils/telemetry/events.js';
|
||||
import { useSurveyState } from './useSurveyState.js';
|
||||
import type { FeedbackSurveyResponse } from './utils.js';
|
||||
const HIDE_THANKS_AFTER_MS = 3000;
|
||||
const POST_COMPACT_SURVEY_GATE = 'tengu_post_compact_survey';
|
||||
const SURVEY_PROBABILITY = 0.2; // Show survey 20% of the time after compaction
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'
|
||||
import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { shouldUseSessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { isCompactBoundaryMessage } from '../../utils/messages.js'
|
||||
import { logOTelEvent } from '../../utils/telemetry/events.js'
|
||||
import { useSurveyState } from './useSurveyState.js'
|
||||
import type { FeedbackSurveyResponse } from './utils.js'
|
||||
|
||||
function hasMessageAfterBoundary(messages: Message[], boundaryUuid: string): boolean {
|
||||
const boundaryIndex = messages.findIndex(msg => msg.uuid === boundaryUuid);
|
||||
const HIDE_THANKS_AFTER_MS = 3000
|
||||
const POST_COMPACT_SURVEY_GATE = 'tengu_post_compact_survey'
|
||||
const SURVEY_PROBABILITY = 0.2 // Show survey 20% of the time after compaction
|
||||
|
||||
function hasMessageAfterBoundary(
|
||||
messages: Message[],
|
||||
boundaryUuid: string,
|
||||
): boolean {
|
||||
const boundaryIndex = messages.findIndex(msg => msg.uuid === boundaryUuid)
|
||||
if (boundaryIndex === -1) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if there's a user or assistant message after the boundary
|
||||
for (let i = boundaryIndex + 1; i < messages.length; i++) {
|
||||
const msg = messages[i];
|
||||
const msg = messages[i]
|
||||
if (msg && (msg.type === 'user' || msg.type === 'assistant')) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
export function usePostCompactSurvey(messages, isLoading, t0, t1) {
|
||||
const $ = _c(23);
|
||||
const hasActivePrompt = t0 === undefined ? false : t0;
|
||||
let t2;
|
||||
if ($[0] !== t1) {
|
||||
t2 = t1 === undefined ? {} : t1;
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const {
|
||||
enabled: t3
|
||||
} = t2;
|
||||
const enabled = t3 === undefined ? true : t3;
|
||||
const [gateEnabled, setGateEnabled] = useState(null);
|
||||
let t4;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = new Set();
|
||||
$[2] = t4;
|
||||
} else {
|
||||
t4 = $[2];
|
||||
}
|
||||
const seenCompactBoundaries = useRef(t4);
|
||||
const pendingCompactBoundaryUuid = useRef(null);
|
||||
const onOpen = _temp;
|
||||
const onSelect = _temp2;
|
||||
let t5;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = {
|
||||
hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
|
||||
onOpen,
|
||||
onSelect
|
||||
};
|
||||
$[3] = t5;
|
||||
} else {
|
||||
t5 = $[3];
|
||||
}
|
||||
const {
|
||||
state,
|
||||
lastResponse,
|
||||
open,
|
||||
handleSelect
|
||||
} = useSurveyState(t5);
|
||||
let t6;
|
||||
let t7;
|
||||
if ($[4] !== enabled) {
|
||||
t6 = () => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
setGateEnabled(checkStatsigFeatureGate_CACHED_MAY_BE_STALE(POST_COMPACT_SURVEY_GATE));
|
||||
};
|
||||
t7 = [enabled];
|
||||
$[4] = enabled;
|
||||
$[5] = t6;
|
||||
$[6] = t7;
|
||||
} else {
|
||||
t6 = $[5];
|
||||
t7 = $[6];
|
||||
}
|
||||
useEffect(t6, t7);
|
||||
let t8;
|
||||
if ($[7] !== messages) {
|
||||
t8 = new Set(messages.filter(_temp3).map(_temp4));
|
||||
$[7] = messages;
|
||||
$[8] = t8;
|
||||
} else {
|
||||
t8 = $[8];
|
||||
}
|
||||
const currentCompactBoundaries = t8;
|
||||
let t10;
|
||||
let t9;
|
||||
if ($[9] !== currentCompactBoundaries || $[10] !== enabled || $[11] !== gateEnabled || $[12] !== hasActivePrompt || $[13] !== isLoading || $[14] !== messages || $[15] !== open || $[16] !== state) {
|
||||
t9 = () => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
if (state !== "closed" || isLoading) {
|
||||
return;
|
||||
}
|
||||
if (hasActivePrompt) {
|
||||
return;
|
||||
}
|
||||
if (gateEnabled !== true) {
|
||||
return;
|
||||
}
|
||||
if (isFeedbackSurveyDisabled()) {
|
||||
return;
|
||||
}
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
|
||||
return;
|
||||
}
|
||||
if (pendingCompactBoundaryUuid.current !== null) {
|
||||
if (hasMessageAfterBoundary(messages, pendingCompactBoundaryUuid.current)) {
|
||||
pendingCompactBoundaryUuid.current = null;
|
||||
if (Math.random() < SURVEY_PROBABILITY) {
|
||||
open();
|
||||
}
|
||||
return;
|
||||
|
||||
export function usePostCompactSurvey(
|
||||
messages: Message[],
|
||||
isLoading: boolean,
|
||||
hasActivePrompt = false,
|
||||
{ enabled = true }: { enabled?: boolean } = {},
|
||||
): {
|
||||
state:
|
||||
| 'closed'
|
||||
| 'open'
|
||||
| 'thanks'
|
||||
| 'transcript_prompt'
|
||||
| 'submitting'
|
||||
| 'submitted'
|
||||
lastResponse: FeedbackSurveyResponse | null
|
||||
handleSelect: (selected: FeedbackSurveyResponse) => void
|
||||
} {
|
||||
const [gateEnabled, setGateEnabled] = useState<boolean | null>(null)
|
||||
const seenCompactBoundaries = useRef<Set<string>>(new Set())
|
||||
// Track the compact boundary we're waiting on (to show survey after next message)
|
||||
const pendingCompactBoundaryUuid = useRef<string | null>(null)
|
||||
|
||||
const onOpen = useCallback((appearanceId: string) => {
|
||||
const smCompactionEnabled = shouldUseSessionMemoryCompaction()
|
||||
logEvent('tengu_post_compact_survey_event', {
|
||||
event_type:
|
||||
'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id:
|
||||
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
session_memory_compaction_enabled:
|
||||
smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
void logOTelEvent('feedback_survey', {
|
||||
event_type: 'appeared',
|
||||
appearance_id: appearanceId,
|
||||
survey_type: 'post_compact',
|
||||
})
|
||||
}, [])
|
||||
|
||||
const onSelect = useCallback(
|
||||
(appearanceId: string, selected: FeedbackSurveyResponse) => {
|
||||
const smCompactionEnabled = shouldUseSessionMemoryCompaction()
|
||||
logEvent('tengu_post_compact_survey_event', {
|
||||
event_type:
|
||||
'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id:
|
||||
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
response:
|
||||
selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
session_memory_compaction_enabled:
|
||||
smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
void logOTelEvent('feedback_survey', {
|
||||
event_type: 'responded',
|
||||
appearance_id: appearanceId,
|
||||
response: selected,
|
||||
survey_type: 'post_compact',
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const { state, lastResponse, open, handleSelect } = useSurveyState({
|
||||
hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
|
||||
onOpen,
|
||||
onSelect,
|
||||
})
|
||||
|
||||
// Check the feature gate on mount
|
||||
useEffect(() => {
|
||||
if (!enabled) return
|
||||
setGateEnabled(
|
||||
checkStatsigFeatureGate_CACHED_MAY_BE_STALE(POST_COMPACT_SURVEY_GATE),
|
||||
)
|
||||
}, [enabled])
|
||||
|
||||
// Find compact boundary messages
|
||||
const currentCompactBoundaries = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
messages
|
||||
.filter(msg => isCompactBoundaryMessage(msg))
|
||||
.map(msg => msg.uuid),
|
||||
),
|
||||
[messages],
|
||||
)
|
||||
|
||||
// Detect new compact boundaries and defer showing survey until next message
|
||||
useEffect(() => {
|
||||
if (!enabled) return
|
||||
|
||||
// Don't process if already showing
|
||||
if (state !== 'closed' || isLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
// Don't show survey when permission or ask question prompts are visible
|
||||
if (hasActivePrompt) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the gate is enabled
|
||||
if (gateEnabled !== true) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isFeedbackSurveyDisabled()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if survey is explicitly disabled
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
|
||||
return
|
||||
}
|
||||
|
||||
// First, check if we have a pending compact and a new message has arrived
|
||||
if (pendingCompactBoundaryUuid.current !== null) {
|
||||
if (
|
||||
hasMessageAfterBoundary(messages, pendingCompactBoundaryUuid.current)
|
||||
) {
|
||||
// A new message arrived after the compact - decide whether to show survey
|
||||
pendingCompactBoundaryUuid.current = null
|
||||
|
||||
// Only show survey 20% of the time
|
||||
if (Math.random() < SURVEY_PROBABILITY) {
|
||||
open()
|
||||
}
|
||||
return
|
||||
}
|
||||
const newBoundaries = Array.from(currentCompactBoundaries).filter(uuid => !seenCompactBoundaries.current.has(uuid));
|
||||
if (newBoundaries.length > 0) {
|
||||
seenCompactBoundaries.current = new Set(currentCompactBoundaries);
|
||||
pendingCompactBoundaryUuid.current = newBoundaries[newBoundaries.length - 1];
|
||||
}
|
||||
};
|
||||
t10 = [enabled, currentCompactBoundaries, state, isLoading, hasActivePrompt, gateEnabled, messages, open];
|
||||
$[9] = currentCompactBoundaries;
|
||||
$[10] = enabled;
|
||||
$[11] = gateEnabled;
|
||||
$[12] = hasActivePrompt;
|
||||
$[13] = isLoading;
|
||||
$[14] = messages;
|
||||
$[15] = open;
|
||||
$[16] = state;
|
||||
$[17] = t10;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
t10 = $[17];
|
||||
t9 = $[18];
|
||||
}
|
||||
useEffect(t9, t10);
|
||||
let t11;
|
||||
if ($[19] !== handleSelect || $[20] !== lastResponse || $[21] !== state) {
|
||||
t11 = {
|
||||
state,
|
||||
lastResponse,
|
||||
handleSelect
|
||||
};
|
||||
$[19] = handleSelect;
|
||||
$[20] = lastResponse;
|
||||
$[21] = state;
|
||||
$[22] = t11;
|
||||
} else {
|
||||
t11 = $[22];
|
||||
}
|
||||
return t11;
|
||||
}
|
||||
function _temp4(msg_0) {
|
||||
return msg_0.uuid;
|
||||
}
|
||||
function _temp3(msg) {
|
||||
return isCompactBoundaryMessage(msg);
|
||||
}
|
||||
function _temp2(appearanceId_0, selected) {
|
||||
const smCompactionEnabled_0 = shouldUseSessionMemoryCompaction();
|
||||
logEvent("tengu_post_compact_survey_event", {
|
||||
event_type: "responded" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
session_memory_compaction_enabled: smCompactionEnabled_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
logOTelEvent("feedback_survey", {
|
||||
event_type: "responded",
|
||||
appearance_id: appearanceId_0,
|
||||
response: selected,
|
||||
survey_type: "post_compact"
|
||||
});
|
||||
}
|
||||
function _temp(appearanceId) {
|
||||
const smCompactionEnabled = shouldUseSessionMemoryCompaction();
|
||||
logEvent("tengu_post_compact_survey_event", {
|
||||
event_type: "appeared" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
session_memory_compaction_enabled: smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
logOTelEvent("feedback_survey", {
|
||||
event_type: "appeared",
|
||||
appearance_id: appearanceId,
|
||||
survey_type: "post_compact"
|
||||
});
|
||||
}
|
||||
|
||||
// Find new compact boundaries that we haven't seen yet
|
||||
const newBoundaries = Array.from(currentCompactBoundaries).filter(
|
||||
uuid => !seenCompactBoundaries.current.has(uuid),
|
||||
)
|
||||
|
||||
if (newBoundaries.length > 0) {
|
||||
// Mark these boundaries as seen
|
||||
seenCompactBoundaries.current = new Set(currentCompactBoundaries)
|
||||
|
||||
// Don't show survey immediately - wait for next message
|
||||
// Store the most recent new boundary UUID
|
||||
pendingCompactBoundaryUuid.current =
|
||||
newBoundaries[newBoundaries.length - 1]!
|
||||
}
|
||||
}, [
|
||||
enabled,
|
||||
currentCompactBoundaries,
|
||||
state,
|
||||
isLoading,
|
||||
hasActivePrompt,
|
||||
gateEnabled,
|
||||
messages,
|
||||
open,
|
||||
])
|
||||
|
||||
return { state, lastResponse, handleSelect }
|
||||
}
|
||||
|
||||
@@ -1,99 +1,144 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
|
||||
import type { FeedbackSurveyResponse } from './utils.js';
|
||||
type SurveyState = 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
|
||||
import { randomUUID } from 'crypto'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
|
||||
import type { FeedbackSurveyResponse } from './utils.js'
|
||||
|
||||
type SurveyState =
|
||||
| 'closed'
|
||||
| 'open'
|
||||
| 'thanks'
|
||||
| 'transcript_prompt'
|
||||
| 'submitting'
|
||||
| 'submitted'
|
||||
|
||||
type UseSurveyStateOptions = {
|
||||
hideThanksAfterMs: number;
|
||||
onOpen: (appearanceId: string) => void | Promise<void>;
|
||||
onSelect: (appearanceId: string, selected: FeedbackSurveyResponse) => void | Promise<void>;
|
||||
shouldShowTranscriptPrompt?: (selected: FeedbackSurveyResponse) => boolean;
|
||||
onTranscriptPromptShown?: (appearanceId: string, surveyResponse: FeedbackSurveyResponse) => void;
|
||||
onTranscriptSelect?: (appearanceId: string, selected: TranscriptShareResponse, surveyResponse: FeedbackSurveyResponse | null) => boolean | Promise<boolean>;
|
||||
};
|
||||
hideThanksAfterMs: number
|
||||
onOpen: (appearanceId: string) => void | Promise<void>
|
||||
onSelect: (
|
||||
appearanceId: string,
|
||||
selected: FeedbackSurveyResponse,
|
||||
) => void | Promise<void>
|
||||
shouldShowTranscriptPrompt?: (selected: FeedbackSurveyResponse) => boolean
|
||||
onTranscriptPromptShown?: (
|
||||
appearanceId: string,
|
||||
surveyResponse: FeedbackSurveyResponse,
|
||||
) => void
|
||||
onTranscriptSelect?: (
|
||||
appearanceId: string,
|
||||
selected: TranscriptShareResponse,
|
||||
surveyResponse: FeedbackSurveyResponse | null,
|
||||
) => boolean | Promise<boolean>
|
||||
}
|
||||
|
||||
export function useSurveyState({
|
||||
hideThanksAfterMs,
|
||||
onOpen,
|
||||
onSelect,
|
||||
shouldShowTranscriptPrompt,
|
||||
onTranscriptPromptShown,
|
||||
onTranscriptSelect
|
||||
onTranscriptSelect,
|
||||
}: UseSurveyStateOptions): {
|
||||
state: SurveyState;
|
||||
lastResponse: FeedbackSurveyResponse | null;
|
||||
open: () => void;
|
||||
handleSelect: (selected: FeedbackSurveyResponse) => boolean;
|
||||
handleTranscriptSelect: (selected: TranscriptShareResponse) => void;
|
||||
state: SurveyState
|
||||
lastResponse: FeedbackSurveyResponse | null
|
||||
open: () => void
|
||||
handleSelect: (selected: FeedbackSurveyResponse) => boolean
|
||||
handleTranscriptSelect: (selected: TranscriptShareResponse) => void
|
||||
} {
|
||||
const [state, setState] = useState<SurveyState>('closed');
|
||||
const [lastResponse, setLastResponse] = useState<FeedbackSurveyResponse | null>(null);
|
||||
const appearanceId = useRef(randomUUID());
|
||||
const lastResponseRef = useRef<FeedbackSurveyResponse | null>(null);
|
||||
const [state, setState] = useState<SurveyState>('closed')
|
||||
const [lastResponse, setLastResponse] =
|
||||
useState<FeedbackSurveyResponse | null>(null)
|
||||
const appearanceId = useRef(randomUUID())
|
||||
const lastResponseRef = useRef<FeedbackSurveyResponse | null>(null)
|
||||
|
||||
const showThanksThenClose = useCallback(() => {
|
||||
setState('thanks');
|
||||
setTimeout((setState_0, setLastResponse_0) => {
|
||||
setState_0('closed');
|
||||
setLastResponse_0(null);
|
||||
}, hideThanksAfterMs, setState, setLastResponse);
|
||||
}, [hideThanksAfterMs]);
|
||||
setState('thanks')
|
||||
setTimeout(
|
||||
(setState, setLastResponse) => {
|
||||
setState('closed')
|
||||
setLastResponse(null)
|
||||
},
|
||||
hideThanksAfterMs,
|
||||
setState,
|
||||
setLastResponse,
|
||||
)
|
||||
}, [hideThanksAfterMs])
|
||||
|
||||
const showSubmittedThenClose = useCallback(() => {
|
||||
setState('submitted');
|
||||
setTimeout(setState, hideThanksAfterMs, 'closed');
|
||||
}, [hideThanksAfterMs]);
|
||||
setState('submitted')
|
||||
setTimeout(setState, hideThanksAfterMs, 'closed')
|
||||
}, [hideThanksAfterMs])
|
||||
|
||||
const open = useCallback(() => {
|
||||
if (state !== 'closed') {
|
||||
return;
|
||||
return
|
||||
}
|
||||
setState('open');
|
||||
appearanceId.current = randomUUID();
|
||||
void onOpen(appearanceId.current);
|
||||
}, [state, onOpen]);
|
||||
const handleSelect = useCallback((selected: FeedbackSurveyResponse): boolean => {
|
||||
setLastResponse(selected);
|
||||
lastResponseRef.current = selected;
|
||||
// Always fire the survey response event first
|
||||
void onSelect(appearanceId.current, selected);
|
||||
if (selected === 'dismissed') {
|
||||
setState('closed');
|
||||
setLastResponse(null);
|
||||
} else if (shouldShowTranscriptPrompt?.(selected)) {
|
||||
setState('transcript_prompt');
|
||||
onTranscriptPromptShown?.(appearanceId.current, selected);
|
||||
return true;
|
||||
} else {
|
||||
showThanksThenClose();
|
||||
}
|
||||
return false;
|
||||
}, [showThanksThenClose, onSelect, shouldShowTranscriptPrompt, onTranscriptPromptShown]);
|
||||
const handleTranscriptSelect = useCallback((selected_0: TranscriptShareResponse) => {
|
||||
switch (selected_0) {
|
||||
case 'yes':
|
||||
setState('submitting');
|
||||
void (async () => {
|
||||
try {
|
||||
const success = await onTranscriptSelect?.(appearanceId.current, selected_0, lastResponseRef.current);
|
||||
if (success) {
|
||||
showSubmittedThenClose();
|
||||
} else {
|
||||
showThanksThenClose();
|
||||
setState('open')
|
||||
appearanceId.current = randomUUID()
|
||||
void onOpen(appearanceId.current)
|
||||
}, [state, onOpen])
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(selected: FeedbackSurveyResponse): boolean => {
|
||||
setLastResponse(selected)
|
||||
lastResponseRef.current = selected
|
||||
// Always fire the survey response event first
|
||||
void onSelect(appearanceId.current, selected)
|
||||
|
||||
if (selected === 'dismissed') {
|
||||
setState('closed')
|
||||
setLastResponse(null)
|
||||
} else if (shouldShowTranscriptPrompt?.(selected)) {
|
||||
setState('transcript_prompt')
|
||||
onTranscriptPromptShown?.(appearanceId.current, selected)
|
||||
return true
|
||||
} else {
|
||||
showThanksThenClose()
|
||||
}
|
||||
return false
|
||||
},
|
||||
[
|
||||
showThanksThenClose,
|
||||
onSelect,
|
||||
shouldShowTranscriptPrompt,
|
||||
onTranscriptPromptShown,
|
||||
],
|
||||
)
|
||||
|
||||
const handleTranscriptSelect = useCallback(
|
||||
(selected: TranscriptShareResponse) => {
|
||||
switch (selected) {
|
||||
case 'yes':
|
||||
setState('submitting')
|
||||
void (async () => {
|
||||
try {
|
||||
const success = await onTranscriptSelect?.(
|
||||
appearanceId.current,
|
||||
selected,
|
||||
lastResponseRef.current,
|
||||
)
|
||||
if (success) {
|
||||
showSubmittedThenClose()
|
||||
} else {
|
||||
showThanksThenClose()
|
||||
}
|
||||
} catch {
|
||||
showThanksThenClose()
|
||||
}
|
||||
} catch {
|
||||
showThanksThenClose();
|
||||
}
|
||||
})();
|
||||
break;
|
||||
case 'no':
|
||||
case 'dont_ask_again':
|
||||
void onTranscriptSelect?.(appearanceId.current, selected_0, lastResponseRef.current);
|
||||
showThanksThenClose();
|
||||
break;
|
||||
}
|
||||
}, [showThanksThenClose, showSubmittedThenClose, onTranscriptSelect]);
|
||||
return {
|
||||
state,
|
||||
lastResponse,
|
||||
open,
|
||||
handleSelect,
|
||||
handleTranscriptSelect
|
||||
};
|
||||
})()
|
||||
break
|
||||
case 'no':
|
||||
case 'dont_ask_again':
|
||||
void onTranscriptSelect?.(
|
||||
appearanceId.current,
|
||||
selected,
|
||||
lastResponseRef.current,
|
||||
)
|
||||
showThanksThenClose()
|
||||
break
|
||||
}
|
||||
},
|
||||
[showThanksThenClose, showSubmittedThenClose, onTranscriptSelect],
|
||||
)
|
||||
|
||||
return { state, lastResponse, open, handleSelect, handleTranscriptSelect }
|
||||
}
|
||||
|
||||
@@ -1,50 +1,38 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import TextInput from '../TextInput.js';
|
||||
import * as React from 'react'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import TextInput from '../TextInput.js'
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
historyFailedMatch: boolean;
|
||||
};
|
||||
function HistorySearchInput(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
historyFailedMatch
|
||||
} = t0;
|
||||
const t1 = historyFailedMatch ? "no matching prompt:" : "search prompts:";
|
||||
let t2;
|
||||
if ($[0] !== t1) {
|
||||
t2 = <Text dimColor={true}>{t1}</Text>;
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const t3 = stringWidth(value) + 1;
|
||||
let t4;
|
||||
if ($[2] !== onChange || $[3] !== t3 || $[4] !== value) {
|
||||
t4 = <TextInput value={value} onChange={onChange} cursorOffset={value.length} onChangeCursorOffset={_temp} columns={t3} focus={true} showCursor={true} multiline={false} dimColor={true} />;
|
||||
$[2] = onChange;
|
||||
$[3] = t3;
|
||||
$[4] = value;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
let t5;
|
||||
if ($[6] !== t2 || $[7] !== t4) {
|
||||
t5 = <Box gap={1}>{t2}{t4}</Box>;
|
||||
$[6] = t2;
|
||||
$[7] = t4;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
return t5;
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
historyFailedMatch: boolean
|
||||
}
|
||||
function _temp() {}
|
||||
export default HistorySearchInput;
|
||||
|
||||
function HistorySearchInput({
|
||||
value,
|
||||
onChange,
|
||||
historyFailedMatch,
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<Box gap={1}>
|
||||
<Text dimColor>
|
||||
{historyFailedMatch ? 'no matching prompt:' : 'search prompts:'}
|
||||
</Text>
|
||||
<TextInput
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
// Force cursor to end of search input since navigation should cancel search
|
||||
cursorOffset={value.length}
|
||||
onChangeCursorOffset={() => {}}
|
||||
columns={stringWidth(value) + 1}
|
||||
focus={true}
|
||||
showCursor={true}
|
||||
multiline={false}
|
||||
dimColor={true}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default HistorySearchInput
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
import * as React from 'react';
|
||||
import { FLAG_ICON } from '../../constants/figures.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import * as React from 'react'
|
||||
import { FLAG_ICON } from '../../constants/figures.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
/**
|
||||
* ANT-ONLY: Banner shown in the transcript that prompts users to report
|
||||
* issues via /issue. Appears when friction is detected in the conversation.
|
||||
*/
|
||||
export function IssueFlagBanner() {
|
||||
return null;
|
||||
export function IssueFlagBanner(): React.ReactNode {
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" marginTop={1} width="100%">
|
||||
<Box minWidth={2}>
|
||||
<Text color="warning">{FLAG_ICON}</Text>
|
||||
</Box>
|
||||
<Text>
|
||||
<Text dimColor>[ANT-ONLY] </Text>
|
||||
<Text color="warning" bold>
|
||||
Something off with Claude?
|
||||
</Text>
|
||||
<Text dimColor> /issue to report it</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,218 +1,201 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
import { type Notification, useNotifications } from 'src/context/notifications.js';
|
||||
import { logEvent } from 'src/services/analytics/index.js';
|
||||
import { useAppState } from 'src/state/AppState.js';
|
||||
import { useVoiceState } from '../../context/voice.js';
|
||||
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js';
|
||||
import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js';
|
||||
import type { IDESelection } from '../../hooks/useIdeSelection.js';
|
||||
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
|
||||
import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js';
|
||||
import { calculateTokenWarningState } from '../../services/compact/autoCompact.js';
|
||||
import type { MCPServerConnection } from '../../services/mcp/types.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import { getApiKeyHelperElapsedMs, getConfiguredApiKeyHelper, getSubscriptionType } from '../../utils/auth.js';
|
||||
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js';
|
||||
import { getExternalEditor } from '../../utils/editor.js';
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
||||
import { formatDuration } from '../../utils/format.js';
|
||||
import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js';
|
||||
import { toIDEDisplayName } from '../../utils/ide.js';
|
||||
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js';
|
||||
import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js';
|
||||
import { AutoUpdaterWrapper } from '../AutoUpdaterWrapper.js';
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
||||
import { IdeStatusIndicator } from '../IdeStatusIndicator.js';
|
||||
import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js';
|
||||
import { SentryErrorBoundary } from '../SentryErrorBoundary.js';
|
||||
import { TokenWarning } from '../TokenWarning.js';
|
||||
import { SandboxPromptFooterHint } from './SandboxPromptFooterHint.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import { type ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
type Notification,
|
||||
useNotifications,
|
||||
} from 'src/context/notifications.js'
|
||||
import { logEvent } from 'src/services/analytics/index.js'
|
||||
import { useAppState } from 'src/state/AppState.js'
|
||||
import { useVoiceState } from '../../context/voice.js'
|
||||
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
|
||||
import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js'
|
||||
import type { IDESelection } from '../../hooks/useIdeSelection.js'
|
||||
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
|
||||
import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'
|
||||
import { calculateTokenWarningState } from '../../services/compact/autoCompact.js'
|
||||
import type { MCPServerConnection } from '../../services/mcp/types.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import {
|
||||
getApiKeyHelperElapsedMs,
|
||||
getConfiguredApiKeyHelper,
|
||||
getSubscriptionType,
|
||||
} from '../../utils/auth.js'
|
||||
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
|
||||
import { getExternalEditor } from '../../utils/editor.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { formatDuration } from '../../utils/format.js'
|
||||
import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js'
|
||||
import { toIDEDisplayName } from '../../utils/ide.js'
|
||||
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'
|
||||
import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js'
|
||||
import { AutoUpdaterWrapper } from '../AutoUpdaterWrapper.js'
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||
import { IdeStatusIndicator } from '../IdeStatusIndicator.js'
|
||||
import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js'
|
||||
import { SentryErrorBoundary } from '../SentryErrorBoundary.js'
|
||||
import { TokenWarning } from '../TokenWarning.js'
|
||||
import { SandboxPromptFooterHint } from './SandboxPromptFooterHint.js'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const VoiceIndicator: typeof import('./VoiceIndicator.js').VoiceIndicator = feature('VOICE_MODE') ? require('./VoiceIndicator.js').VoiceIndicator : () => null;
|
||||
const VoiceIndicator: typeof import('./VoiceIndicator.js').VoiceIndicator =
|
||||
feature('VOICE_MODE')
|
||||
? require('./VoiceIndicator.js').VoiceIndicator
|
||||
: () => null
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
export const FOOTER_TEMPORARY_STATUS_TIMEOUT = 5000;
|
||||
export const FOOTER_TEMPORARY_STATUS_TIMEOUT = 5000
|
||||
|
||||
type Props = {
|
||||
apiKeyStatus: VerificationStatus;
|
||||
autoUpdaterResult: AutoUpdaterResult | null;
|
||||
isAutoUpdating: boolean;
|
||||
debug: boolean;
|
||||
verbose: boolean;
|
||||
messages: Message[];
|
||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void;
|
||||
ideSelection: IDESelection | undefined;
|
||||
mcpClients?: MCPServerConnection[];
|
||||
isInputWrapped?: boolean;
|
||||
isNarrow?: boolean;
|
||||
};
|
||||
export function Notifications(t0) {
|
||||
const $ = _c(34);
|
||||
const {
|
||||
apiKeyStatus,
|
||||
autoUpdaterResult,
|
||||
debug,
|
||||
isAutoUpdating,
|
||||
verbose,
|
||||
messages,
|
||||
onAutoUpdaterResult,
|
||||
onChangeIsUpdating,
|
||||
ideSelection,
|
||||
mcpClients,
|
||||
isInputWrapped: t1,
|
||||
isNarrow: t2
|
||||
} = t0;
|
||||
const isInputWrapped = t1 === undefined ? false : t1;
|
||||
const isNarrow = t2 === undefined ? false : t2;
|
||||
let t3;
|
||||
if ($[0] !== messages) {
|
||||
const messagesForTokenCount = getMessagesAfterCompactBoundary(messages);
|
||||
t3 = tokenCountFromLastAPIResponse(messagesForTokenCount);
|
||||
$[0] = messages;
|
||||
$[1] = t3;
|
||||
} else {
|
||||
t3 = $[1];
|
||||
}
|
||||
const tokenUsage = t3;
|
||||
const mainLoopModel = useMainLoopModel();
|
||||
let t4;
|
||||
if ($[2] !== mainLoopModel || $[3] !== tokenUsage) {
|
||||
t4 = calculateTokenWarningState(tokenUsage, mainLoopModel);
|
||||
$[2] = mainLoopModel;
|
||||
$[3] = tokenUsage;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
const isShowingCompactMessage = t4.isAboveWarningThreshold;
|
||||
const {
|
||||
status: ideStatus
|
||||
} = useIdeConnectionStatus(mcpClients);
|
||||
const notifications = useAppState(_temp);
|
||||
const {
|
||||
apiKeyStatus: VerificationStatus
|
||||
autoUpdaterResult: AutoUpdaterResult | null
|
||||
isAutoUpdating: boolean
|
||||
debug: boolean
|
||||
verbose: boolean
|
||||
messages: Message[]
|
||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void
|
||||
ideSelection: IDESelection | undefined
|
||||
mcpClients?: MCPServerConnection[]
|
||||
isInputWrapped?: boolean
|
||||
isNarrow?: boolean
|
||||
}
|
||||
|
||||
export function Notifications({
|
||||
apiKeyStatus,
|
||||
autoUpdaterResult,
|
||||
debug,
|
||||
isAutoUpdating,
|
||||
verbose,
|
||||
messages,
|
||||
onAutoUpdaterResult,
|
||||
onChangeIsUpdating,
|
||||
ideSelection,
|
||||
mcpClients,
|
||||
isInputWrapped = false,
|
||||
isNarrow = false,
|
||||
}: Props): ReactNode {
|
||||
const tokenUsage = useMemo(() => {
|
||||
const messagesForTokenCount = getMessagesAfterCompactBoundary(messages)
|
||||
return tokenCountFromLastAPIResponse(messagesForTokenCount)
|
||||
}, [messages])
|
||||
|
||||
// AppState-sourced model — same source as API requests. getMainLoopModel()
|
||||
// re-reads settings.json on every call, so another session's /model write
|
||||
// would leak into this session's display (anthropics/claude-code#37596).
|
||||
const mainLoopModel = useMainLoopModel()
|
||||
const isShowingCompactMessage = calculateTokenWarningState(
|
||||
tokenUsage,
|
||||
mainLoopModel,
|
||||
).isAboveWarningThreshold
|
||||
const { status: ideStatus } = useIdeConnectionStatus(mcpClients)
|
||||
const notifications = useAppState(s => s.notifications)
|
||||
const { addNotification, removeNotification } = useNotifications()
|
||||
const claudeAiLimits = useClaudeAiLimits()
|
||||
|
||||
// Register env hook notifier for CwdChanged/FileChanged feedback
|
||||
useEffect(() => {
|
||||
setEnvHookNotifier((text, isError) => {
|
||||
addNotification({
|
||||
key: 'env-hook',
|
||||
text,
|
||||
color: isError ? 'error' : undefined,
|
||||
priority: isError ? 'medium' : 'low',
|
||||
timeoutMs: isError ? 8000 : 5000,
|
||||
})
|
||||
})
|
||||
return () => setEnvHookNotifier(null)
|
||||
}, [addNotification])
|
||||
|
||||
// Check if we should show the IDE selection indicator
|
||||
const shouldShowIdeSelection =
|
||||
ideStatus === 'connected' &&
|
||||
(ideSelection?.filePath ||
|
||||
(ideSelection?.text && ideSelection.lineCount > 0))
|
||||
|
||||
// Hide update installed message when showing IDE selection
|
||||
const shouldShowAutoUpdater =
|
||||
!shouldShowIdeSelection ||
|
||||
isAutoUpdating ||
|
||||
autoUpdaterResult?.status !== 'success'
|
||||
|
||||
// Check if we're in overage mode for UI indicators
|
||||
const isInOverageMode = claudeAiLimits.isUsingOverage
|
||||
const subscriptionType = getSubscriptionType()
|
||||
const isTeamOrEnterprise =
|
||||
subscriptionType === 'team' || subscriptionType === 'enterprise'
|
||||
|
||||
// Check if the external editor hint should be shown
|
||||
const editor = getExternalEditor()
|
||||
const shouldShowExternalEditorHint =
|
||||
isInputWrapped &&
|
||||
!isShowingCompactMessage &&
|
||||
apiKeyStatus !== 'invalid' &&
|
||||
apiKeyStatus !== 'missing' &&
|
||||
editor !== undefined
|
||||
|
||||
// Show external editor hint as notification when input is wrapped
|
||||
useEffect(() => {
|
||||
if (shouldShowExternalEditorHint && editor) {
|
||||
logEvent('tengu_external_editor_hint_shown', {})
|
||||
addNotification({
|
||||
key: 'external-editor-hint',
|
||||
jsx: (
|
||||
<Text dimColor>
|
||||
<ConfigurableShortcutHint
|
||||
action="chat:externalEditor"
|
||||
context="Chat"
|
||||
fallback="ctrl+g"
|
||||
description={`edit in ${toIDEDisplayName(editor)}`}
|
||||
/>
|
||||
</Text>
|
||||
),
|
||||
priority: 'immediate',
|
||||
timeoutMs: 5000,
|
||||
})
|
||||
} else {
|
||||
removeNotification('external-editor-hint')
|
||||
}
|
||||
}, [
|
||||
shouldShowExternalEditorHint,
|
||||
editor,
|
||||
addNotification,
|
||||
removeNotification
|
||||
} = useNotifications();
|
||||
const claudeAiLimits = useClaudeAiLimits();
|
||||
let t5;
|
||||
let t6;
|
||||
if ($[5] !== addNotification) {
|
||||
t5 = () => {
|
||||
setEnvHookNotifier((text, isError) => {
|
||||
addNotification({
|
||||
key: "env-hook",
|
||||
text,
|
||||
color: isError ? "error" : undefined,
|
||||
priority: isError ? "medium" : "low",
|
||||
timeoutMs: isError ? 8000 : 5000
|
||||
});
|
||||
});
|
||||
return _temp2;
|
||||
};
|
||||
t6 = [addNotification];
|
||||
$[5] = addNotification;
|
||||
$[6] = t5;
|
||||
$[7] = t6;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
t6 = $[7];
|
||||
}
|
||||
useEffect(t5, t6);
|
||||
const shouldShowIdeSelection = ideStatus === "connected" && (ideSelection?.filePath || ideSelection?.text && ideSelection.lineCount > 0);
|
||||
const shouldShowAutoUpdater = !shouldShowIdeSelection || isAutoUpdating || autoUpdaterResult?.status !== "success";
|
||||
const isInOverageMode = claudeAiLimits.isUsingOverage;
|
||||
let t7;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = getSubscriptionType();
|
||||
$[8] = t7;
|
||||
} else {
|
||||
t7 = $[8];
|
||||
}
|
||||
const subscriptionType = t7;
|
||||
const isTeamOrEnterprise = subscriptionType === "team" || subscriptionType === "enterprise";
|
||||
let t8;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = getExternalEditor();
|
||||
$[9] = t8;
|
||||
} else {
|
||||
t8 = $[9];
|
||||
}
|
||||
const editor = t8;
|
||||
const shouldShowExternalEditorHint = isInputWrapped && !isShowingCompactMessage && apiKeyStatus !== "invalid" && apiKeyStatus !== "missing" && editor !== undefined;
|
||||
let t10;
|
||||
let t9;
|
||||
if ($[10] !== addNotification || $[11] !== removeNotification || $[12] !== shouldShowExternalEditorHint) {
|
||||
t9 = () => {
|
||||
if (shouldShowExternalEditorHint && editor) {
|
||||
logEvent("tengu_external_editor_hint_shown", {});
|
||||
addNotification({
|
||||
key: "external-editor-hint",
|
||||
jsx: <Text dimColor={true}><ConfigurableShortcutHint action="chat:externalEditor" context="Chat" fallback="ctrl+g" description={`edit in ${toIDEDisplayName(editor)}`} /></Text>,
|
||||
priority: "immediate",
|
||||
timeoutMs: 5000
|
||||
});
|
||||
} else {
|
||||
removeNotification("external-editor-hint");
|
||||
}
|
||||
};
|
||||
t10 = [shouldShowExternalEditorHint, editor, addNotification, removeNotification];
|
||||
$[10] = addNotification;
|
||||
$[11] = removeNotification;
|
||||
$[12] = shouldShowExternalEditorHint;
|
||||
$[13] = t10;
|
||||
$[14] = t9;
|
||||
} else {
|
||||
t10 = $[13];
|
||||
t9 = $[14];
|
||||
}
|
||||
useEffect(t9, t10);
|
||||
const t11 = isNarrow ? "flex-start" : "flex-end";
|
||||
const t12 = isInOverageMode ?? false;
|
||||
let t13;
|
||||
if ($[15] !== apiKeyStatus || $[16] !== autoUpdaterResult || $[17] !== debug || $[18] !== ideSelection || $[19] !== isAutoUpdating || $[20] !== isShowingCompactMessage || $[21] !== mainLoopModel || $[22] !== mcpClients || $[23] !== notifications || $[24] !== onAutoUpdaterResult || $[25] !== onChangeIsUpdating || $[26] !== shouldShowAutoUpdater || $[27] !== t12 || $[28] !== tokenUsage || $[29] !== verbose) {
|
||||
t13 = <NotificationContent ideSelection={ideSelection} mcpClients={mcpClients} notifications={notifications} isInOverageMode={t12} isTeamOrEnterprise={isTeamOrEnterprise} apiKeyStatus={apiKeyStatus} debug={debug} verbose={verbose} tokenUsage={tokenUsage} mainLoopModel={mainLoopModel} shouldShowAutoUpdater={shouldShowAutoUpdater} autoUpdaterResult={autoUpdaterResult} isAutoUpdating={isAutoUpdating} isShowingCompactMessage={isShowingCompactMessage} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={onChangeIsUpdating} />;
|
||||
$[15] = apiKeyStatus;
|
||||
$[16] = autoUpdaterResult;
|
||||
$[17] = debug;
|
||||
$[18] = ideSelection;
|
||||
$[19] = isAutoUpdating;
|
||||
$[20] = isShowingCompactMessage;
|
||||
$[21] = mainLoopModel;
|
||||
$[22] = mcpClients;
|
||||
$[23] = notifications;
|
||||
$[24] = onAutoUpdaterResult;
|
||||
$[25] = onChangeIsUpdating;
|
||||
$[26] = shouldShowAutoUpdater;
|
||||
$[27] = t12;
|
||||
$[28] = tokenUsage;
|
||||
$[29] = verbose;
|
||||
$[30] = t13;
|
||||
} else {
|
||||
t13 = $[30];
|
||||
}
|
||||
let t14;
|
||||
if ($[31] !== t11 || $[32] !== t13) {
|
||||
t14 = <SentryErrorBoundary><Box flexDirection="column" alignItems={t11} flexShrink={0} overflowX="hidden">{t13}</Box></SentryErrorBoundary>;
|
||||
$[31] = t11;
|
||||
$[32] = t13;
|
||||
$[33] = t14;
|
||||
} else {
|
||||
t14 = $[33];
|
||||
}
|
||||
return t14;
|
||||
}
|
||||
function _temp2() {
|
||||
return setEnvHookNotifier(null);
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.notifications;
|
||||
removeNotification,
|
||||
])
|
||||
|
||||
return (
|
||||
<SentryErrorBoundary>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
||||
flexShrink={0}
|
||||
overflowX="hidden"
|
||||
>
|
||||
<NotificationContent
|
||||
ideSelection={ideSelection}
|
||||
mcpClients={mcpClients}
|
||||
notifications={notifications}
|
||||
isInOverageMode={isInOverageMode ?? false}
|
||||
isTeamOrEnterprise={isTeamOrEnterprise}
|
||||
apiKeyStatus={apiKeyStatus}
|
||||
debug={debug}
|
||||
verbose={verbose}
|
||||
tokenUsage={tokenUsage}
|
||||
mainLoopModel={mainLoopModel}
|
||||
shouldShowAutoUpdater={shouldShowAutoUpdater}
|
||||
autoUpdaterResult={autoUpdaterResult}
|
||||
isAutoUpdating={isAutoUpdating}
|
||||
isShowingCompactMessage={isShowingCompactMessage}
|
||||
onAutoUpdaterResult={onAutoUpdaterResult}
|
||||
onChangeIsUpdating={onChangeIsUpdating}
|
||||
/>
|
||||
</Box>
|
||||
</SentryErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
function NotificationContent({
|
||||
ideSelection,
|
||||
mcpClients,
|
||||
@@ -229,103 +212,155 @@ function NotificationContent({
|
||||
isAutoUpdating,
|
||||
isShowingCompactMessage,
|
||||
onAutoUpdaterResult,
|
||||
onChangeIsUpdating
|
||||
onChangeIsUpdating,
|
||||
}: {
|
||||
ideSelection: IDESelection | undefined;
|
||||
mcpClients?: MCPServerConnection[];
|
||||
ideSelection: IDESelection | undefined
|
||||
mcpClients?: MCPServerConnection[]
|
||||
notifications: {
|
||||
current: Notification | null;
|
||||
queue: Notification[];
|
||||
};
|
||||
isInOverageMode: boolean;
|
||||
isTeamOrEnterprise: boolean;
|
||||
apiKeyStatus: VerificationStatus;
|
||||
debug: boolean;
|
||||
verbose: boolean;
|
||||
tokenUsage: number;
|
||||
mainLoopModel: string;
|
||||
shouldShowAutoUpdater: boolean;
|
||||
autoUpdaterResult: AutoUpdaterResult | null;
|
||||
isAutoUpdating: boolean;
|
||||
isShowingCompactMessage: boolean;
|
||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void;
|
||||
current: Notification | null
|
||||
queue: Notification[]
|
||||
}
|
||||
isInOverageMode: boolean
|
||||
isTeamOrEnterprise: boolean
|
||||
apiKeyStatus: VerificationStatus
|
||||
debug: boolean
|
||||
verbose: boolean
|
||||
tokenUsage: number
|
||||
mainLoopModel: string
|
||||
shouldShowAutoUpdater: boolean
|
||||
autoUpdaterResult: AutoUpdaterResult | null
|
||||
isAutoUpdating: boolean
|
||||
isShowingCompactMessage: boolean
|
||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void
|
||||
}): ReactNode {
|
||||
// Poll apiKeyHelper inflight state to show slow-helper notice.
|
||||
// Gated on configuration — most users never set apiKeyHelper, so the
|
||||
// effect is a no-op for them (no interval allocated).
|
||||
const [apiKeyHelperSlow, setApiKeyHelperSlow] = useState<string | null>(null);
|
||||
const [apiKeyHelperSlow, setApiKeyHelperSlow] = useState<string | null>(null)
|
||||
useEffect(() => {
|
||||
if (!getConfiguredApiKeyHelper()) return;
|
||||
const interval = setInterval((setSlow: React.Dispatch<React.SetStateAction<string | null>>) => {
|
||||
const ms = getApiKeyHelperElapsedMs();
|
||||
const next = ms >= 10_000 ? formatDuration(ms) : null;
|
||||
setSlow(prev => next === prev ? prev : next);
|
||||
}, 1000, setApiKeyHelperSlow);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
if (!getConfiguredApiKeyHelper()) return
|
||||
const interval = setInterval(
|
||||
(setSlow: React.Dispatch<React.SetStateAction<string | null>>) => {
|
||||
const ms = getApiKeyHelperElapsedMs()
|
||||
const next = ms >= 10_000 ? formatDuration(ms) : null
|
||||
setSlow(prev => (next === prev ? prev : next))
|
||||
},
|
||||
1000,
|
||||
setApiKeyHelperSlow,
|
||||
)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
// Voice state (VOICE_MODE builds only, runtime-gated by GrowthBook)
|
||||
const voiceState = feature('VOICE_MODE') ?
|
||||
const voiceState = feature('VOICE_MODE')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useVoiceState(s => s.voiceState)
|
||||
: ('idle' as const)
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useVoiceState(s => s.voiceState) : 'idle' as const;
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false;
|
||||
const voiceError = feature('VOICE_MODE') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useVoiceState(s_0 => s_0.voiceError) : null;
|
||||
const isBriefOnly = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s_1 => s_1.isBriefOnly) : false;
|
||||
const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false
|
||||
const voiceError = feature('VOICE_MODE')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useVoiceState(s => s.voiceError)
|
||||
: null
|
||||
const isBriefOnly =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s => s.isBriefOnly)
|
||||
: false
|
||||
|
||||
// When voice is actively recording or processing, replace all
|
||||
// notifications with just the voice indicator.
|
||||
if (feature('VOICE_MODE') && voiceEnabled && (voiceState === 'recording' || voiceState === 'processing')) {
|
||||
return <VoiceIndicator voiceState={voiceState} />;
|
||||
if (
|
||||
feature('VOICE_MODE') &&
|
||||
voiceEnabled &&
|
||||
(voiceState === 'recording' || voiceState === 'processing')
|
||||
) {
|
||||
return <VoiceIndicator voiceState={voiceState} />
|
||||
}
|
||||
return <>
|
||||
|
||||
return (
|
||||
<>
|
||||
<IdeStatusIndicator ideSelection={ideSelection} mcpClients={mcpClients} />
|
||||
{notifications.current && ('jsx' in notifications.current ? <Text wrap="truncate" key={notifications.current.key}>
|
||||
{notifications.current &&
|
||||
('jsx' in notifications.current ? (
|
||||
<Text wrap="truncate" key={notifications.current.key}>
|
||||
{notifications.current.jsx}
|
||||
</Text> : <Text color={notifications.current.color} dimColor={!notifications.current.color} wrap="truncate">
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
color={notifications.current.color}
|
||||
dimColor={!notifications.current.color}
|
||||
wrap="truncate"
|
||||
>
|
||||
{notifications.current.text}
|
||||
</Text>)}
|
||||
{isInOverageMode && !isTeamOrEnterprise && <Box>
|
||||
</Text>
|
||||
))}
|
||||
{isInOverageMode && !isTeamOrEnterprise && (
|
||||
<Box>
|
||||
<Text dimColor wrap="truncate">
|
||||
Now using extra usage
|
||||
</Text>
|
||||
</Box>}
|
||||
{apiKeyHelperSlow && <Box>
|
||||
</Box>
|
||||
)}
|
||||
{apiKeyHelperSlow && (
|
||||
<Box>
|
||||
<Text color="warning" wrap="truncate">
|
||||
apiKeyHelper is taking a while{' '}
|
||||
</Text>
|
||||
<Text dimColor wrap="truncate">
|
||||
({apiKeyHelperSlow})
|
||||
</Text>
|
||||
</Box>}
|
||||
{(apiKeyStatus === 'invalid' || apiKeyStatus === 'missing') && <Box>
|
||||
</Box>
|
||||
)}
|
||||
{(apiKeyStatus === 'invalid' || apiKeyStatus === 'missing') && (
|
||||
<Box>
|
||||
<Text color="error" wrap="truncate">
|
||||
{isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ? 'Authentication error · Try again' : 'Not logged in · Run /login'}
|
||||
{isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)
|
||||
? 'Authentication error · Try again'
|
||||
: 'Not logged in · Run /login'}
|
||||
</Text>
|
||||
</Box>}
|
||||
{debug && <Box>
|
||||
</Box>
|
||||
)}
|
||||
{debug && (
|
||||
<Box>
|
||||
<Text color="warning" wrap="truncate">
|
||||
Debug mode
|
||||
</Text>
|
||||
</Box>}
|
||||
{apiKeyStatus !== 'invalid' && apiKeyStatus !== 'missing' && verbose && <Box>
|
||||
</Box>
|
||||
)}
|
||||
{apiKeyStatus !== 'invalid' && apiKeyStatus !== 'missing' && verbose && (
|
||||
<Box>
|
||||
<Text dimColor wrap="truncate">
|
||||
{tokenUsage} tokens
|
||||
</Text>
|
||||
</Box>}
|
||||
{!isBriefOnly && <TokenWarning tokenUsage={tokenUsage} model={mainLoopModel} />}
|
||||
{shouldShowAutoUpdater && <AutoUpdaterWrapper verbose={verbose} onAutoUpdaterResult={onAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} isUpdating={isAutoUpdating} onChangeIsUpdating={onChangeIsUpdating} showSuccessMessage={!isShowingCompactMessage} />}
|
||||
{feature('VOICE_MODE') ? voiceEnabled && voiceError && <Box>
|
||||
</Box>
|
||||
)}
|
||||
{!isBriefOnly && (
|
||||
<TokenWarning tokenUsage={tokenUsage} model={mainLoopModel} />
|
||||
)}
|
||||
{shouldShowAutoUpdater && (
|
||||
<AutoUpdaterWrapper
|
||||
verbose={verbose}
|
||||
onAutoUpdaterResult={onAutoUpdaterResult}
|
||||
autoUpdaterResult={autoUpdaterResult}
|
||||
isUpdating={isAutoUpdating}
|
||||
onChangeIsUpdating={onChangeIsUpdating}
|
||||
showSuccessMessage={!isShowingCompactMessage}
|
||||
/>
|
||||
)}
|
||||
{feature('VOICE_MODE')
|
||||
? voiceEnabled &&
|
||||
voiceError && (
|
||||
<Box>
|
||||
<Text color="error" wrap="truncate">
|
||||
{voiceError}
|
||||
</Text>
|
||||
</Box> : null}
|
||||
</Box>
|
||||
)
|
||||
: null}
|
||||
<MemoryUsageIndicator />
|
||||
<SandboxPromptFooterHint />
|
||||
</>;
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +1,77 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import { memo, type ReactNode, useMemo, useRef } from 'react';
|
||||
import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js';
|
||||
import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js';
|
||||
import { useSetPromptOverlay } from '../../context/promptOverlayContext.js';
|
||||
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js';
|
||||
import type { IDESelection } from '../../hooks/useIdeSelection.js';
|
||||
import { useSettings } from '../../hooks/useSettings.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { MCPServerConnection } from '../../services/mcp/types.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import type { ToolPermissionContext } from '../../Tool.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js';
|
||||
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js';
|
||||
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js';
|
||||
import { isUndercover } from '../../utils/undercover.js';
|
||||
import { CoordinatorTaskPanel, useCoordinatorTaskCount } from '../CoordinatorAgentStatus.js';
|
||||
import { getLastAssistantMessageId, StatusLine, statusLineShouldDisplay } from '../StatusLine.js';
|
||||
import { Notifications } from './Notifications.js';
|
||||
import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js';
|
||||
import { PromptInputFooterSuggestions, type SuggestionItem } from './PromptInputFooterSuggestions.js';
|
||||
import { PromptInputHelpMenu } from './PromptInputHelpMenu.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import { memo, type ReactNode, useMemo, useRef } from 'react'
|
||||
import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js'
|
||||
import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js'
|
||||
import { useSetPromptOverlay } from '../../context/promptOverlayContext.js'
|
||||
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
|
||||
import type { IDESelection } from '../../hooks/useIdeSelection.js'
|
||||
import { useSettings } from '../../hooks/useSettings.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { MCPServerConnection } from '../../services/mcp/types.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import type { ToolPermissionContext } from '../../Tool.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js'
|
||||
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
|
||||
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
|
||||
import { isUndercover } from '../../utils/undercover.js'
|
||||
import {
|
||||
CoordinatorTaskPanel,
|
||||
useCoordinatorTaskCount,
|
||||
} from '../CoordinatorAgentStatus.js'
|
||||
import {
|
||||
getLastAssistantMessageId,
|
||||
StatusLine,
|
||||
statusLineShouldDisplay,
|
||||
} from '../StatusLine.js'
|
||||
import { Notifications } from './Notifications.js'
|
||||
import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js'
|
||||
import {
|
||||
PromptInputFooterSuggestions,
|
||||
type SuggestionItem,
|
||||
} from './PromptInputFooterSuggestions.js'
|
||||
import { PromptInputHelpMenu } from './PromptInputHelpMenu.js'
|
||||
|
||||
type Props = {
|
||||
apiKeyStatus: VerificationStatus;
|
||||
debug: boolean;
|
||||
apiKeyStatus: VerificationStatus
|
||||
debug: boolean
|
||||
exitMessage: {
|
||||
show: boolean;
|
||||
key?: string;
|
||||
};
|
||||
vimMode: VimMode | undefined;
|
||||
mode: PromptInputMode;
|
||||
autoUpdaterResult: AutoUpdaterResult | null;
|
||||
isAutoUpdating: boolean;
|
||||
verbose: boolean;
|
||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void;
|
||||
suggestions: SuggestionItem[];
|
||||
selectedSuggestion: number;
|
||||
maxColumnWidth?: number;
|
||||
toolPermissionContext: ToolPermissionContext;
|
||||
helpOpen: boolean;
|
||||
suppressHint: boolean;
|
||||
isLoading: boolean;
|
||||
tasksSelected: boolean;
|
||||
teamsSelected: boolean;
|
||||
bridgeSelected: boolean;
|
||||
tmuxSelected: boolean;
|
||||
teammateFooterIndex?: number;
|
||||
ideSelection: IDESelection | undefined;
|
||||
mcpClients?: MCPServerConnection[];
|
||||
isPasting?: boolean;
|
||||
isInputWrapped?: boolean;
|
||||
messages: Message[];
|
||||
isSearching: boolean;
|
||||
historyQuery: string;
|
||||
setHistoryQuery: (query: string) => void;
|
||||
historyFailedMatch: boolean;
|
||||
onOpenTasksDialog?: (taskId?: string) => void;
|
||||
};
|
||||
show: boolean
|
||||
key?: string
|
||||
}
|
||||
vimMode: VimMode | undefined
|
||||
mode: PromptInputMode
|
||||
autoUpdaterResult: AutoUpdaterResult | null
|
||||
isAutoUpdating: boolean
|
||||
verbose: boolean
|
||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void
|
||||
suggestions: SuggestionItem[]
|
||||
selectedSuggestion: number
|
||||
maxColumnWidth?: number
|
||||
toolPermissionContext: ToolPermissionContext
|
||||
helpOpen: boolean
|
||||
suppressHint: boolean
|
||||
isLoading: boolean
|
||||
tasksSelected: boolean
|
||||
teamsSelected: boolean
|
||||
bridgeSelected: boolean
|
||||
tmuxSelected: boolean
|
||||
teammateFooterIndex?: number
|
||||
ideSelection: IDESelection | undefined
|
||||
mcpClients?: MCPServerConnection[]
|
||||
isPasting?: boolean
|
||||
isInputWrapped?: boolean
|
||||
messages: Message[]
|
||||
isSearching: boolean
|
||||
historyQuery: string
|
||||
setHistoryQuery: (query: string) => void
|
||||
historyFailedMatch: boolean
|
||||
onOpenTasksDialog?: (taskId?: string) => void
|
||||
}
|
||||
|
||||
function PromptInputFooter({
|
||||
apiKeyStatus,
|
||||
debug,
|
||||
@@ -92,99 +104,176 @@ function PromptInputFooter({
|
||||
historyQuery,
|
||||
setHistoryQuery,
|
||||
historyFailedMatch,
|
||||
onOpenTasksDialog
|
||||
onOpenTasksDialog,
|
||||
}: Props): ReactNode {
|
||||
const settings = useSettings();
|
||||
const {
|
||||
columns,
|
||||
rows
|
||||
} = useTerminalSize();
|
||||
const messagesRef = useRef(messages);
|
||||
messagesRef.current = messages;
|
||||
const lastAssistantMessageId = useMemo(() => getLastAssistantMessageId(messages), [messages]);
|
||||
const isNarrow = columns < 80;
|
||||
const settings = useSettings()
|
||||
const { columns, rows } = useTerminalSize()
|
||||
const messagesRef = useRef(messages)
|
||||
messagesRef.current = messages
|
||||
const lastAssistantMessageId = useMemo(
|
||||
() => getLastAssistantMessageId(messages),
|
||||
[messages],
|
||||
)
|
||||
const isNarrow = columns < 80
|
||||
// In fullscreen the bottom slot is flexShrink:0, so every row here is a row
|
||||
// stolen from the ScrollBox. Drop the optional StatusLine first. Non-fullscreen
|
||||
// has terminal scrollback to absorb overflow, so we never hide StatusLine there.
|
||||
const isFullscreen = isFullscreenEnvEnabled();
|
||||
const isShort = isFullscreen && rows < 24;
|
||||
const isFullscreen = isFullscreenEnvEnabled()
|
||||
const isShort = isFullscreen && rows < 24
|
||||
|
||||
// Pill highlights when tasks is the active footer item AND no specific
|
||||
// agent row is selected. When coordinatorTaskIndex >= 0 the pointer has
|
||||
// moved into CoordinatorTaskPanel, so the pill should un-highlight.
|
||||
// coordinatorTaskCount === 0 covers the bash-only case (no agent rows
|
||||
// exist, pill is the only selectable item).
|
||||
const coordinatorTaskCount = useCoordinatorTaskCount();
|
||||
const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex);
|
||||
const pillSelected = tasksSelected && (coordinatorTaskCount === 0 || coordinatorTaskIndex < 0);
|
||||
const coordinatorTaskCount = useCoordinatorTaskCount()
|
||||
const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex)
|
||||
const pillSelected =
|
||||
tasksSelected && (coordinatorTaskCount === 0 || coordinatorTaskIndex < 0)
|
||||
|
||||
// Hide `? for shortcuts` if the user has a custom status line, or during ctrl-r
|
||||
const suppressHint = suppressHintFromProps || statusLineShouldDisplay(settings) || isSearching;
|
||||
const suppressHint =
|
||||
suppressHintFromProps || statusLineShouldDisplay(settings) || isSearching
|
||||
// Fullscreen: portal data to FullscreenLayout — see promptOverlayContext.tsx
|
||||
const overlayData = useMemo(() => isFullscreen && suggestions.length ? {
|
||||
suggestions,
|
||||
selectedSuggestion,
|
||||
maxColumnWidth
|
||||
} : null, [isFullscreen, suggestions, selectedSuggestion, maxColumnWidth]);
|
||||
useSetPromptOverlay(overlayData);
|
||||
const overlayData = useMemo(
|
||||
() =>
|
||||
isFullscreen && suggestions.length
|
||||
? { suggestions, selectedSuggestion, maxColumnWidth }
|
||||
: null,
|
||||
[isFullscreen, suggestions, selectedSuggestion, maxColumnWidth],
|
||||
)
|
||||
useSetPromptOverlay(overlayData)
|
||||
|
||||
if (suggestions.length && !isFullscreen) {
|
||||
return <Box paddingX={2} paddingY={0}>
|
||||
<PromptInputFooterSuggestions suggestions={suggestions} selectedSuggestion={selectedSuggestion} maxColumnWidth={maxColumnWidth} />
|
||||
</Box>;
|
||||
return (
|
||||
<Box paddingX={2} paddingY={0}>
|
||||
<PromptInputFooterSuggestions
|
||||
suggestions={suggestions}
|
||||
selectedSuggestion={selectedSuggestion}
|
||||
maxColumnWidth={maxColumnWidth}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (helpOpen) {
|
||||
return <PromptInputHelpMenu dimColor={true} fixedWidth={true} paddingX={2} />;
|
||||
return (
|
||||
<PromptInputHelpMenu dimColor={true} fixedWidth={true} paddingX={2} />
|
||||
)
|
||||
}
|
||||
return <>
|
||||
<Box flexDirection={isNarrow ? 'column' : 'row'} justifyContent={isNarrow ? 'flex-start' : 'space-between'} paddingX={2} gap={isNarrow ? 0 : 1}>
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
flexDirection={isNarrow ? 'column' : 'row'}
|
||||
justifyContent={isNarrow ? 'flex-start' : 'space-between'}
|
||||
paddingX={2}
|
||||
gap={isNarrow ? 0 : 1}
|
||||
>
|
||||
<Box flexDirection="column" flexShrink={isNarrow ? 0 : 1}>
|
||||
{mode === 'prompt' && !isShort && !exitMessage.show && !isPasting && statusLineShouldDisplay(settings) && <StatusLine messagesRef={messagesRef} lastAssistantMessageId={lastAssistantMessageId} vimMode={vimMode} />}
|
||||
<PromptInputFooterLeftSide exitMessage={exitMessage} vimMode={vimMode} mode={mode} toolPermissionContext={toolPermissionContext} suppressHint={suppressHint} isLoading={isLoading} tasksSelected={pillSelected} teamsSelected={teamsSelected} teammateFooterIndex={teammateFooterIndex} tmuxSelected={tmuxSelected} isPasting={isPasting} isSearching={isSearching} historyQuery={historyQuery} setHistoryQuery={setHistoryQuery} historyFailedMatch={historyFailedMatch} onOpenTasksDialog={onOpenTasksDialog} />
|
||||
{mode === 'prompt' &&
|
||||
!isShort &&
|
||||
!exitMessage.show &&
|
||||
!isPasting &&
|
||||
statusLineShouldDisplay(settings) && (
|
||||
<StatusLine
|
||||
messagesRef={messagesRef}
|
||||
lastAssistantMessageId={lastAssistantMessageId}
|
||||
vimMode={vimMode}
|
||||
/>
|
||||
)}
|
||||
<PromptInputFooterLeftSide
|
||||
exitMessage={exitMessage}
|
||||
vimMode={vimMode}
|
||||
mode={mode}
|
||||
toolPermissionContext={toolPermissionContext}
|
||||
suppressHint={suppressHint}
|
||||
isLoading={isLoading}
|
||||
tasksSelected={pillSelected}
|
||||
teamsSelected={teamsSelected}
|
||||
teammateFooterIndex={teammateFooterIndex}
|
||||
tmuxSelected={tmuxSelected}
|
||||
isPasting={isPasting}
|
||||
isSearching={isSearching}
|
||||
historyQuery={historyQuery}
|
||||
setHistoryQuery={setHistoryQuery}
|
||||
historyFailedMatch={historyFailedMatch}
|
||||
onOpenTasksDialog={onOpenTasksDialog}
|
||||
/>
|
||||
</Box>
|
||||
<Box flexShrink={1} gap={1}>
|
||||
{isFullscreen ? null : <Notifications apiKeyStatus={apiKeyStatus} autoUpdaterResult={autoUpdaterResult} debug={debug} isAutoUpdating={isAutoUpdating} verbose={verbose} messages={messages} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={onChangeIsUpdating} ideSelection={ideSelection} mcpClients={mcpClients} isInputWrapped={isInputWrapped} isNarrow={isNarrow} />}
|
||||
{(process.env.USER_TYPE) === 'ant' && isUndercover() && <Text dimColor>undercover</Text>}
|
||||
{isFullscreen ? null : (
|
||||
<Notifications
|
||||
apiKeyStatus={apiKeyStatus}
|
||||
autoUpdaterResult={autoUpdaterResult}
|
||||
debug={debug}
|
||||
isAutoUpdating={isAutoUpdating}
|
||||
verbose={verbose}
|
||||
messages={messages}
|
||||
onAutoUpdaterResult={onAutoUpdaterResult}
|
||||
onChangeIsUpdating={onChangeIsUpdating}
|
||||
ideSelection={ideSelection}
|
||||
mcpClients={mcpClients}
|
||||
isInputWrapped={isInputWrapped}
|
||||
isNarrow={isNarrow}
|
||||
/>
|
||||
)}
|
||||
{process.env.USER_TYPE === 'ant' && isUndercover() && (
|
||||
<Text dimColor>undercover</Text>
|
||||
)}
|
||||
<BridgeStatusIndicator bridgeSelected={bridgeSelected} />
|
||||
</Box>
|
||||
</Box>
|
||||
{(process.env.USER_TYPE) === 'ant' && <CoordinatorTaskPanel />}
|
||||
</>;
|
||||
{process.env.USER_TYPE === 'ant' && <CoordinatorTaskPanel />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default memo(PromptInputFooter);
|
||||
|
||||
export default memo(PromptInputFooter)
|
||||
|
||||
type BridgeStatusProps = {
|
||||
bridgeSelected: boolean;
|
||||
};
|
||||
bridgeSelected: boolean
|
||||
}
|
||||
|
||||
function BridgeStatusIndicator({
|
||||
bridgeSelected
|
||||
bridgeSelected,
|
||||
}: BridgeStatusProps): React.ReactNode {
|
||||
if (!feature('BRIDGE_MODE')) return null;
|
||||
if (!feature('BRIDGE_MODE')) return null
|
||||
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
const enabled = useAppState(s => s.replBridgeEnabled);
|
||||
const enabled = useAppState(s => s.replBridgeEnabled)
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
const connected = useAppState(s_0 => s_0.replBridgeConnected);
|
||||
const connected = useAppState(s => s.replBridgeConnected)
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
const sessionActive = useAppState(s_1 => s_1.replBridgeSessionActive);
|
||||
const sessionActive = useAppState(s => s.replBridgeSessionActive)
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
const reconnecting = useAppState(s_2 => s_2.replBridgeReconnecting);
|
||||
const reconnecting = useAppState(s => s.replBridgeReconnecting)
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
const explicit = useAppState(s_3 => s_3.replBridgeExplicit);
|
||||
const explicit = useAppState(s => s.replBridgeExplicit)
|
||||
|
||||
// Failed state is surfaced via notification (useReplBridge), not a footer pill.
|
||||
if (!isBridgeEnabled() || !enabled) return null;
|
||||
if (!isBridgeEnabled() || !enabled) return null
|
||||
|
||||
const status = getBridgeStatus({
|
||||
error: undefined,
|
||||
connected,
|
||||
sessionActive,
|
||||
reconnecting
|
||||
});
|
||||
reconnecting,
|
||||
})
|
||||
|
||||
// For implicit (config-driven) remote, only show the reconnecting state
|
||||
if (!explicit && status.label !== 'Remote Control reconnecting') {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return <Text color={bridgeSelected ? 'background' : status.color} inverse={bridgeSelected} wrap="truncate">
|
||||
|
||||
return (
|
||||
<Text
|
||||
color={bridgeSelected ? 'background' : status.color}
|
||||
inverse={bridgeSelected}
|
||||
wrap="truncate"
|
||||
>
|
||||
{status.label}
|
||||
{bridgeSelected && <Text dimColor> · Enter to view</Text>}
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,292 +1,248 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { memo, type ReactNode } from 'react';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
import * as React from 'react'
|
||||
import { memo, type ReactNode } from 'react'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js'
|
||||
import type { Theme } from '../../utils/theme.js'
|
||||
|
||||
export type SuggestionItem = {
|
||||
id: string;
|
||||
displayText: string;
|
||||
tag?: string;
|
||||
description?: string;
|
||||
metadata?: unknown;
|
||||
color?: keyof Theme;
|
||||
};
|
||||
export type SuggestionType = 'command' | 'file' | 'directory' | 'agent' | 'shell' | 'custom-title' | 'slack-channel' | 'none';
|
||||
export const OVERLAY_MAX_ITEMS = 5;
|
||||
id: string
|
||||
displayText: string
|
||||
tag?: string
|
||||
description?: string
|
||||
metadata?: unknown
|
||||
color?: keyof Theme
|
||||
}
|
||||
|
||||
export type SuggestionType =
|
||||
| 'command'
|
||||
| 'file'
|
||||
| 'directory'
|
||||
| 'agent'
|
||||
| 'shell'
|
||||
| 'custom-title'
|
||||
| 'slack-channel'
|
||||
| 'none'
|
||||
|
||||
export const OVERLAY_MAX_ITEMS = 5
|
||||
|
||||
/**
|
||||
* Get the icon for a suggestion based on its type
|
||||
* Icons: + for files, ◇ for MCP resources, * for agents
|
||||
*/
|
||||
function getIcon(itemId: string): string {
|
||||
if (itemId.startsWith('file-')) return '+';
|
||||
if (itemId.startsWith('mcp-resource-')) return '◇';
|
||||
if (itemId.startsWith('agent-')) return '*';
|
||||
return '+';
|
||||
if (itemId.startsWith('file-')) return '+'
|
||||
if (itemId.startsWith('mcp-resource-')) return '◇'
|
||||
if (itemId.startsWith('agent-')) return '*'
|
||||
return '+'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an item is a unified suggestion type (file, mcp-resource, or agent)
|
||||
*/
|
||||
function isUnifiedSuggestion(itemId: string): boolean {
|
||||
return itemId.startsWith('file-') || itemId.startsWith('mcp-resource-') || itemId.startsWith('agent-');
|
||||
return (
|
||||
itemId.startsWith('file-') ||
|
||||
itemId.startsWith('mcp-resource-') ||
|
||||
itemId.startsWith('agent-')
|
||||
)
|
||||
}
|
||||
const SuggestionItemRow = memo(function SuggestionItemRow(t0: { item: SuggestionItem; maxColumnWidth: number; isSelected: boolean }) {
|
||||
const $ = _c(36);
|
||||
const {
|
||||
item,
|
||||
maxColumnWidth,
|
||||
isSelected
|
||||
} = t0;
|
||||
const columns = useTerminalSize().columns;
|
||||
const isUnified = isUnifiedSuggestion(item.id);
|
||||
|
||||
const SuggestionItemRow = memo(function SuggestionItemRow({
|
||||
item,
|
||||
maxColumnWidth,
|
||||
isSelected,
|
||||
}: {
|
||||
item: SuggestionItem
|
||||
maxColumnWidth?: number
|
||||
isSelected: boolean
|
||||
}): ReactNode {
|
||||
const columns = useTerminalSize().columns
|
||||
const isUnified = isUnifiedSuggestion(item.id)
|
||||
|
||||
// For unified suggestions (file, mcp-resource, agent), use single-line layout with icon
|
||||
if (isUnified) {
|
||||
let t1;
|
||||
if ($[0] !== item.id) {
|
||||
t1 = getIcon(item.id);
|
||||
$[0] = item.id;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const icon = t1;
|
||||
const textColor = isSelected ? "suggestion" : undefined;
|
||||
const dimColor = !isSelected;
|
||||
const isFile = item.id.startsWith("file-");
|
||||
const isMcpResource = item.id.startsWith("mcp-resource-");
|
||||
const separatorWidth = item.description ? 3 : 0;
|
||||
let displayText;
|
||||
const icon = getIcon(item.id)
|
||||
const textColor: keyof Theme | undefined = isSelected
|
||||
? 'suggestion'
|
||||
: undefined
|
||||
const dimColor = !isSelected
|
||||
|
||||
const isFile = item.id.startsWith('file-')
|
||||
const isMcpResource = item.id.startsWith('mcp-resource-')
|
||||
|
||||
// Calculate layout widths
|
||||
// Layout: "X " (2) + displayText + " – " (3) + description + padding (4)
|
||||
const iconWidth = 2 // icon + space (fixed)
|
||||
const paddingWidth = 4
|
||||
const separatorWidth = item.description ? 3 : 0 // ' – ' separator
|
||||
|
||||
// For files, truncate middle of path to show both directory context and filename
|
||||
// For MCP resources, limit displayText to 30 chars (truncate from end)
|
||||
// For agents, no truncation
|
||||
let displayText: string
|
||||
if (isFile) {
|
||||
let t2;
|
||||
if ($[2] !== item.description) {
|
||||
t2 = item.description ? Math.min(20, stringWidth(item.description)) : 0;
|
||||
$[2] = item.description;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const descReserve = t2;
|
||||
const maxPathLength = columns - 2 - 4 - separatorWidth - descReserve;
|
||||
let t3;
|
||||
if ($[4] !== item.displayText || $[5] !== maxPathLength) {
|
||||
t3 = truncatePathMiddle(item.displayText, maxPathLength);
|
||||
$[4] = item.displayText;
|
||||
$[5] = maxPathLength;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
displayText = t3;
|
||||
// Reserve space for description if present, otherwise use all available space
|
||||
const descReserve = item.description
|
||||
? Math.min(20, stringWidth(item.description))
|
||||
: 0
|
||||
const maxPathLength =
|
||||
columns - iconWidth - paddingWidth - separatorWidth - descReserve
|
||||
displayText = truncatePathMiddle(item.displayText, maxPathLength)
|
||||
} else if (isMcpResource) {
|
||||
const maxDisplayTextLength = 30
|
||||
displayText = truncateToWidth(item.displayText, maxDisplayTextLength)
|
||||
} else {
|
||||
if (isMcpResource) {
|
||||
let t2;
|
||||
if ($[7] !== item.displayText) {
|
||||
t2 = truncateToWidth(item.displayText, 30);
|
||||
$[7] = item.displayText;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
displayText = t2;
|
||||
} else {
|
||||
displayText = item.displayText;
|
||||
}
|
||||
displayText = item.displayText
|
||||
}
|
||||
const availableWidth = columns - 2 - stringWidth(displayText) - separatorWidth - 4;
|
||||
let lineContent;
|
||||
|
||||
const availableWidth =
|
||||
columns -
|
||||
iconWidth -
|
||||
stringWidth(displayText) -
|
||||
separatorWidth -
|
||||
paddingWidth
|
||||
|
||||
// Build the full line as a single string to prevent wrapping
|
||||
let lineContent: string
|
||||
if (item.description) {
|
||||
const maxDescLength = Math.max(0, availableWidth);
|
||||
let t2;
|
||||
if ($[9] !== item.description || $[10] !== maxDescLength) {
|
||||
t2 = truncateToWidth(item.description.replace(/\s+/g, " "), maxDescLength);
|
||||
$[9] = item.description;
|
||||
$[10] = maxDescLength;
|
||||
$[11] = t2;
|
||||
} else {
|
||||
t2 = $[11];
|
||||
}
|
||||
const truncatedDesc = t2;
|
||||
lineContent = `${icon} ${displayText} – ${truncatedDesc}`;
|
||||
const maxDescLength = Math.max(0, availableWidth)
|
||||
const truncatedDesc = truncateToWidth(
|
||||
item.description.replace(/\s+/g, ' '),
|
||||
maxDescLength,
|
||||
)
|
||||
lineContent = `${icon} ${displayText} – ${truncatedDesc}`
|
||||
} else {
|
||||
lineContent = `${icon} ${displayText}`;
|
||||
lineContent = `${icon} ${displayText}`
|
||||
}
|
||||
let t2;
|
||||
if ($[12] !== dimColor || $[13] !== lineContent || $[14] !== textColor) {
|
||||
t2 = <Text color={textColor} dimColor={dimColor} wrap="truncate">{lineContent}</Text>;
|
||||
$[12] = dimColor;
|
||||
$[13] = lineContent;
|
||||
$[14] = textColor;
|
||||
$[15] = t2;
|
||||
} else {
|
||||
t2 = $[15];
|
||||
}
|
||||
return t2;
|
||||
|
||||
return (
|
||||
<Text color={textColor} dimColor={dimColor} wrap="truncate">
|
||||
{lineContent}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
const maxNameWidth = Math.floor(columns * 0.4);
|
||||
const displayTextWidth = Math.min(maxColumnWidth ?? stringWidth(item.displayText) + 5, maxNameWidth);
|
||||
const textColor_0 = item.color || (isSelected ? "suggestion" : undefined);
|
||||
const shouldDim = !isSelected;
|
||||
let displayText_0 = item.displayText;
|
||||
if (stringWidth(displayText_0) > displayTextWidth - 2) {
|
||||
const t1 = displayTextWidth - 2;
|
||||
let t2;
|
||||
if ($[16] !== displayText_0 || $[17] !== t1) {
|
||||
t2 = truncateToWidth(displayText_0, t1);
|
||||
$[16] = displayText_0;
|
||||
$[17] = t1;
|
||||
$[18] = t2;
|
||||
} else {
|
||||
t2 = $[18];
|
||||
}
|
||||
displayText_0 = t2;
|
||||
|
||||
// For non-unified suggestions (commands, shell, etc.), use improved layout from main
|
||||
// Cap the command name column at 40% of terminal width to ensure description has space
|
||||
const maxNameWidth = Math.floor(columns * 0.4)
|
||||
const displayTextWidth = Math.min(
|
||||
maxColumnWidth ?? stringWidth(item.displayText) + 5,
|
||||
maxNameWidth,
|
||||
)
|
||||
|
||||
const textColor = item.color || (isSelected ? 'suggestion' : undefined)
|
||||
const shouldDim = !isSelected
|
||||
|
||||
// Truncate and pad the display text to fixed width
|
||||
let displayText = item.displayText
|
||||
if (stringWidth(displayText) > displayTextWidth - 2) {
|
||||
displayText = truncateToWidth(displayText, displayTextWidth - 2)
|
||||
}
|
||||
const paddedDisplayText = displayText_0 + " ".repeat(Math.max(0, displayTextWidth - stringWidth(displayText_0)));
|
||||
const tagText = item.tag ? `[${item.tag}] ` : "";
|
||||
const tagWidth = stringWidth(tagText);
|
||||
const descriptionWidth = Math.max(0, columns - displayTextWidth - tagWidth - 4);
|
||||
let t1;
|
||||
if ($[19] !== descriptionWidth || $[20] !== item.description) {
|
||||
t1 = item.description ? truncateToWidth(item.description.replace(/\s+/g, " "), descriptionWidth) : "";
|
||||
$[19] = descriptionWidth;
|
||||
$[20] = item.description;
|
||||
$[21] = t1;
|
||||
} else {
|
||||
t1 = $[21];
|
||||
}
|
||||
const truncatedDescription = t1;
|
||||
let t2;
|
||||
if ($[22] !== paddedDisplayText || $[23] !== shouldDim || $[24] !== textColor_0) {
|
||||
t2 = <Text color={textColor_0} dimColor={shouldDim}>{paddedDisplayText}</Text>;
|
||||
$[22] = paddedDisplayText;
|
||||
$[23] = shouldDim;
|
||||
$[24] = textColor_0;
|
||||
$[25] = t2;
|
||||
} else {
|
||||
t2 = $[25];
|
||||
}
|
||||
let t3;
|
||||
if ($[26] !== tagText) {
|
||||
t3 = tagText ? <Text dimColor={true}>{tagText}</Text> : null;
|
||||
$[26] = tagText;
|
||||
$[27] = t3;
|
||||
} else {
|
||||
t3 = $[27];
|
||||
}
|
||||
const t4 = isSelected ? "suggestion" : undefined;
|
||||
const t5 = !isSelected;
|
||||
let t6;
|
||||
if ($[28] !== t4 || $[29] !== t5 || $[30] !== truncatedDescription) {
|
||||
t6 = <Text color={t4} dimColor={t5}>{truncatedDescription}</Text>;
|
||||
$[28] = t4;
|
||||
$[29] = t5;
|
||||
$[30] = truncatedDescription;
|
||||
$[31] = t6;
|
||||
} else {
|
||||
t6 = $[31];
|
||||
}
|
||||
let t7;
|
||||
if ($[32] !== t2 || $[33] !== t3 || $[34] !== t6) {
|
||||
t7 = <Text wrap="truncate">{t2}{t3}{t6}</Text>;
|
||||
$[32] = t2;
|
||||
$[33] = t3;
|
||||
$[34] = t6;
|
||||
$[35] = t7;
|
||||
} else {
|
||||
t7 = $[35];
|
||||
}
|
||||
return t7;
|
||||
});
|
||||
const paddedDisplayText =
|
||||
displayText +
|
||||
' '.repeat(Math.max(0, displayTextWidth - stringWidth(displayText)))
|
||||
|
||||
const tagText = item.tag ? `[${item.tag}] ` : ''
|
||||
const tagWidth = stringWidth(tagText)
|
||||
const descriptionWidth = Math.max(
|
||||
0,
|
||||
columns - displayTextWidth - tagWidth - 4,
|
||||
)
|
||||
// Skill descriptions can contain newlines (e.g. /claude-api's "TRIGGER
|
||||
// when:" block). A multi-line row grows the overlay past minHeight; when
|
||||
// the filter narrows past that skill, the overlay shrinks and leaves
|
||||
// ghost rows. Flatten to one line before truncating.
|
||||
const truncatedDescription = item.description
|
||||
? truncateToWidth(item.description.replace(/\s+/g, ' '), descriptionWidth)
|
||||
: ''
|
||||
|
||||
return (
|
||||
<Text wrap="truncate">
|
||||
<Text color={textColor} dimColor={shouldDim}>
|
||||
{paddedDisplayText}
|
||||
</Text>
|
||||
{tagText ? <Text dimColor>{tagText}</Text> : null}
|
||||
<Text
|
||||
color={isSelected ? 'suggestion' : undefined}
|
||||
dimColor={!isSelected}
|
||||
>
|
||||
{truncatedDescription}
|
||||
</Text>
|
||||
</Text>
|
||||
)
|
||||
})
|
||||
|
||||
type Props = {
|
||||
suggestions: SuggestionItem[];
|
||||
selectedSuggestion: number;
|
||||
maxColumnWidth?: number;
|
||||
suggestions: SuggestionItem[]
|
||||
selectedSuggestion: number
|
||||
maxColumnWidth?: number
|
||||
/**
|
||||
* When true, the suggestions are rendered inside a position=absolute
|
||||
* overlay. We omit minHeight and flex-end so the y-clamp in the
|
||||
* renderer doesn't push fewer items down into the prompt area.
|
||||
*/
|
||||
overlay?: boolean;
|
||||
};
|
||||
export function PromptInputFooterSuggestions(t0) {
|
||||
const $ = _c(22);
|
||||
const {
|
||||
suggestions,
|
||||
selectedSuggestion,
|
||||
maxColumnWidth: maxColumnWidthProp,
|
||||
overlay
|
||||
} = t0;
|
||||
const {
|
||||
rows
|
||||
} = useTerminalSize();
|
||||
const maxVisibleItems = overlay ? OVERLAY_MAX_ITEMS : Math.min(6, Math.max(1, rows - 3));
|
||||
overlay?: boolean
|
||||
}
|
||||
|
||||
export function PromptInputFooterSuggestions({
|
||||
suggestions,
|
||||
selectedSuggestion,
|
||||
maxColumnWidth: maxColumnWidthProp,
|
||||
overlay,
|
||||
}: Props): ReactNode {
|
||||
const { rows } = useTerminalSize()
|
||||
// Maximum number of suggestions to show at once (leaving space for prompt).
|
||||
// Overlay mode (fullscreen) uses a fixed 5 — the floating box sits over
|
||||
// the ScrollBox, so terminal height isn't the constraint.
|
||||
const maxVisibleItems = overlay
|
||||
? OVERLAY_MAX_ITEMS
|
||||
: Math.min(6, Math.max(1, rows - 3))
|
||||
|
||||
// No suggestions to display
|
||||
if (suggestions.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t1;
|
||||
if ($[0] !== maxColumnWidthProp || $[1] !== suggestions) {
|
||||
t1 = maxColumnWidthProp ?? Math.max(...suggestions.map(_temp)) + 5;
|
||||
$[0] = maxColumnWidthProp;
|
||||
$[1] = suggestions;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const maxColumnWidth = t1;
|
||||
const startIndex = Math.max(0, Math.min(selectedSuggestion - Math.floor(maxVisibleItems / 2), suggestions.length - maxVisibleItems));
|
||||
const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length);
|
||||
let T0;
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[3] !== endIndex || $[4] !== maxColumnWidth || $[5] !== overlay || $[6] !== selectedSuggestion || $[7] !== startIndex || $[8] !== suggestions) {
|
||||
const visibleItems = suggestions.slice(startIndex, endIndex);
|
||||
T0 = Box;
|
||||
t2 = "column";
|
||||
t3 = overlay ? undefined : "flex-end";
|
||||
let t5;
|
||||
if ($[13] !== maxColumnWidth || $[14] !== selectedSuggestion || $[15] !== suggestions) {
|
||||
t5 = item_0 => <SuggestionItemRow key={item_0.id} item={item_0} maxColumnWidth={maxColumnWidth} isSelected={item_0.id === suggestions[selectedSuggestion]?.id} />;
|
||||
$[13] = maxColumnWidth;
|
||||
$[14] = selectedSuggestion;
|
||||
$[15] = suggestions;
|
||||
$[16] = t5;
|
||||
} else {
|
||||
t5 = $[16];
|
||||
}
|
||||
t4 = visibleItems.map(t5);
|
||||
$[3] = endIndex;
|
||||
$[4] = maxColumnWidth;
|
||||
$[5] = overlay;
|
||||
$[6] = selectedSuggestion;
|
||||
$[7] = startIndex;
|
||||
$[8] = suggestions;
|
||||
$[9] = T0;
|
||||
$[10] = t2;
|
||||
$[11] = t3;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
T0 = $[9];
|
||||
t2 = $[10];
|
||||
t3 = $[11];
|
||||
t4 = $[12];
|
||||
}
|
||||
let t5;
|
||||
if ($[17] !== T0 || $[18] !== t2 || $[19] !== t3 || $[20] !== t4) {
|
||||
t5 = <T0 flexDirection={t2} justifyContent={t3}>{t4}</T0>;
|
||||
$[17] = T0;
|
||||
$[18] = t2;
|
||||
$[19] = t3;
|
||||
$[20] = t4;
|
||||
$[21] = t5;
|
||||
} else {
|
||||
t5 = $[21];
|
||||
}
|
||||
return t5;
|
||||
|
||||
// Use prop if provided (stable width from all commands), otherwise calculate from visible
|
||||
const maxColumnWidth =
|
||||
maxColumnWidthProp ??
|
||||
Math.max(...suggestions.map(item => stringWidth(item.displayText))) + 5
|
||||
|
||||
// Calculate visible items range based on selected index
|
||||
const startIndex = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
selectedSuggestion - Math.floor(maxVisibleItems / 2),
|
||||
suggestions.length - maxVisibleItems,
|
||||
),
|
||||
)
|
||||
const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length)
|
||||
const visibleItems = suggestions.slice(startIndex, endIndex)
|
||||
|
||||
// In non-overlay (inline) mode, justifyContent keeps suggestions
|
||||
// anchored to the bottom (near the prompt). In overlay mode we omit
|
||||
// both minHeight and flex-end: the parent is position=absolute with
|
||||
// bottom='100%', so its y is clamped to 0 by the renderer when it
|
||||
// would go negative. Adding minHeight + flex-end would create empty
|
||||
// padding rows that shift the visible items down into the prompt area
|
||||
// when the list has fewer items than maxVisibleItems.
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
justifyContent={overlay ? undefined : 'flex-end'}
|
||||
>
|
||||
{visibleItems.map(item => (
|
||||
<SuggestionItemRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
maxColumnWidth={maxColumnWidth}
|
||||
isSelected={item.id === suggestions[selectedSuggestion]?.id}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function _temp(item) {
|
||||
return stringWidth(item.displayText);
|
||||
}
|
||||
export default memo(PromptInputFooterSuggestions);
|
||||
|
||||
export default memo(PromptInputFooterSuggestions)
|
||||
|
||||
@@ -1,357 +1,149 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from 'src/ink.js';
|
||||
import { getPlatform } from 'src/utils/platform.js';
|
||||
import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js';
|
||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js';
|
||||
import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js';
|
||||
import { getNewlineInstructions } from './utils.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from 'src/ink.js'
|
||||
import { getPlatform } from 'src/utils/platform.js'
|
||||
import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js'
|
||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js'
|
||||
import { getNewlineInstructions } from './utils.js'
|
||||
|
||||
/** Format a shortcut for display in the help menu (e.g., "ctrl+o" → "ctrl + o") */
|
||||
function formatShortcut(shortcut: string): string {
|
||||
return shortcut.replace(/\+/g, ' + ');
|
||||
return shortcut.replace(/\+/g, ' + ')
|
||||
}
|
||||
|
||||
type Props = {
|
||||
dimColor?: boolean;
|
||||
fixedWidth?: boolean;
|
||||
gap?: number;
|
||||
paddingX?: number;
|
||||
};
|
||||
export function PromptInputHelpMenu(props) {
|
||||
const $ = _c(99);
|
||||
const {
|
||||
dimColor,
|
||||
fixedWidth,
|
||||
gap,
|
||||
paddingX
|
||||
} = props;
|
||||
const t0 = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
|
||||
let t1;
|
||||
if ($[0] !== t0) {
|
||||
t1 = formatShortcut(t0);
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const transcriptShortcut = t1;
|
||||
const t2 = useShortcutDisplay("app:toggleTodos", "Global", "ctrl+t");
|
||||
let t3;
|
||||
if ($[2] !== t2) {
|
||||
t3 = formatShortcut(t2);
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const todosShortcut = t3;
|
||||
const t4 = useShortcutDisplay("chat:undo", "Chat", "ctrl+_");
|
||||
let t5;
|
||||
if ($[4] !== t4) {
|
||||
t5 = formatShortcut(t4);
|
||||
$[4] = t4;
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
}
|
||||
const undoShortcut = t5;
|
||||
const t6 = useShortcutDisplay("chat:stash", "Chat", "ctrl+s");
|
||||
let t7;
|
||||
if ($[6] !== t6) {
|
||||
t7 = formatShortcut(t6);
|
||||
$[6] = t6;
|
||||
$[7] = t7;
|
||||
} else {
|
||||
t7 = $[7];
|
||||
}
|
||||
const stashShortcut = t7;
|
||||
const t8 = useShortcutDisplay("chat:cycleMode", "Chat", "shift+tab");
|
||||
let t9;
|
||||
if ($[8] !== t8) {
|
||||
t9 = formatShortcut(t8);
|
||||
$[8] = t8;
|
||||
$[9] = t9;
|
||||
} else {
|
||||
t9 = $[9];
|
||||
}
|
||||
const cycleModeShortcut = t9;
|
||||
const t10 = useShortcutDisplay("chat:modelPicker", "Chat", "alt+p");
|
||||
let t11;
|
||||
if ($[10] !== t10) {
|
||||
t11 = formatShortcut(t10);
|
||||
$[10] = t10;
|
||||
$[11] = t11;
|
||||
} else {
|
||||
t11 = $[11];
|
||||
}
|
||||
const modelPickerShortcut = t11;
|
||||
const t12 = useShortcutDisplay("chat:fastMode", "Chat", "alt+o");
|
||||
let t13;
|
||||
if ($[12] !== t12) {
|
||||
t13 = formatShortcut(t12);
|
||||
$[12] = t12;
|
||||
$[13] = t13;
|
||||
} else {
|
||||
t13 = $[13];
|
||||
}
|
||||
const fastModeShortcut = t13;
|
||||
const t14 = useShortcutDisplay("chat:externalEditor", "Chat", "ctrl+g");
|
||||
let t15;
|
||||
if ($[14] !== t14) {
|
||||
t15 = formatShortcut(t14);
|
||||
$[14] = t14;
|
||||
$[15] = t15;
|
||||
} else {
|
||||
t15 = $[15];
|
||||
}
|
||||
const externalEditorShortcut = t15;
|
||||
const t16 = useShortcutDisplay("app:toggleTerminal", "Global", "meta+j");
|
||||
let t17;
|
||||
if ($[16] !== t16) {
|
||||
t17 = formatShortcut(t16);
|
||||
$[16] = t16;
|
||||
$[17] = t17;
|
||||
} else {
|
||||
t17 = $[17];
|
||||
}
|
||||
const terminalShortcut = t17;
|
||||
const t18 = useShortcutDisplay("chat:imagePaste", "Chat", "ctrl+v");
|
||||
let t19;
|
||||
if ($[18] !== t18) {
|
||||
t19 = formatShortcut(t18);
|
||||
$[18] = t18;
|
||||
$[19] = t19;
|
||||
} else {
|
||||
t19 = $[19];
|
||||
}
|
||||
const imagePasteShortcut = t19;
|
||||
let t20;
|
||||
if ($[20] !== dimColor || $[21] !== terminalShortcut) {
|
||||
t20 = feature("TERMINAL_PANEL") ? getFeatureValue_CACHED_MAY_BE_STALE("tengu_terminal_panel", false) ? <Box><Text dimColor={dimColor}>{terminalShortcut} for terminal</Text></Box> : null : null;
|
||||
$[20] = dimColor;
|
||||
$[21] = terminalShortcut;
|
||||
$[22] = t20;
|
||||
} else {
|
||||
t20 = $[22];
|
||||
}
|
||||
const terminalShortcutElement = t20;
|
||||
const t21 = fixedWidth ? 24 : undefined;
|
||||
let t22;
|
||||
if ($[23] !== dimColor) {
|
||||
t22 = <Box><Text dimColor={dimColor}>! for bash mode</Text></Box>;
|
||||
$[23] = dimColor;
|
||||
$[24] = t22;
|
||||
} else {
|
||||
t22 = $[24];
|
||||
}
|
||||
let t23;
|
||||
if ($[25] !== dimColor) {
|
||||
t23 = <Box><Text dimColor={dimColor}>/ for commands</Text></Box>;
|
||||
$[25] = dimColor;
|
||||
$[26] = t23;
|
||||
} else {
|
||||
t23 = $[26];
|
||||
}
|
||||
let t24;
|
||||
if ($[27] !== dimColor) {
|
||||
t24 = <Box><Text dimColor={dimColor}>@ for file paths</Text></Box>;
|
||||
$[27] = dimColor;
|
||||
$[28] = t24;
|
||||
} else {
|
||||
t24 = $[28];
|
||||
}
|
||||
let t25;
|
||||
if ($[29] !== dimColor) {
|
||||
t25 = <Box><Text dimColor={dimColor}>{"& for background"}</Text></Box>;
|
||||
$[29] = dimColor;
|
||||
$[30] = t25;
|
||||
} else {
|
||||
t25 = $[30];
|
||||
}
|
||||
let t26;
|
||||
if ($[31] !== dimColor) {
|
||||
t26 = <Box><Text dimColor={dimColor}>/btw for side question</Text></Box>;
|
||||
$[31] = dimColor;
|
||||
$[32] = t26;
|
||||
} else {
|
||||
t26 = $[32];
|
||||
}
|
||||
let t27;
|
||||
if ($[33] !== t21 || $[34] !== t22 || $[35] !== t23 || $[36] !== t24 || $[37] !== t25 || $[38] !== t26) {
|
||||
t27 = <Box flexDirection="column" width={t21}>{t22}{t23}{t24}{t25}{t26}</Box>;
|
||||
$[33] = t21;
|
||||
$[34] = t22;
|
||||
$[35] = t23;
|
||||
$[36] = t24;
|
||||
$[37] = t25;
|
||||
$[38] = t26;
|
||||
$[39] = t27;
|
||||
} else {
|
||||
t27 = $[39];
|
||||
}
|
||||
const t28 = fixedWidth ? 35 : undefined;
|
||||
let t29;
|
||||
if ($[40] !== dimColor) {
|
||||
t29 = <Box><Text dimColor={dimColor}>double tap esc to clear input</Text></Box>;
|
||||
$[40] = dimColor;
|
||||
$[41] = t29;
|
||||
} else {
|
||||
t29 = $[41];
|
||||
}
|
||||
let t30;
|
||||
if ($[42] !== cycleModeShortcut || $[43] !== dimColor) {
|
||||
t30 = <Box><Text dimColor={dimColor}>{cycleModeShortcut}{" "}{false ? "to cycle modes" : "to auto-accept edits"}</Text></Box>;
|
||||
$[42] = cycleModeShortcut;
|
||||
$[43] = dimColor;
|
||||
$[44] = t30;
|
||||
} else {
|
||||
t30 = $[44];
|
||||
}
|
||||
let t31;
|
||||
if ($[45] !== dimColor || $[46] !== transcriptShortcut) {
|
||||
t31 = <Box><Text dimColor={dimColor}>{transcriptShortcut} for verbose output</Text></Box>;
|
||||
$[45] = dimColor;
|
||||
$[46] = transcriptShortcut;
|
||||
$[47] = t31;
|
||||
} else {
|
||||
t31 = $[47];
|
||||
}
|
||||
let t32;
|
||||
if ($[48] !== dimColor || $[49] !== todosShortcut) {
|
||||
t32 = <Box><Text dimColor={dimColor}>{todosShortcut} to toggle tasks</Text></Box>;
|
||||
$[48] = dimColor;
|
||||
$[49] = todosShortcut;
|
||||
$[50] = t32;
|
||||
} else {
|
||||
t32 = $[50];
|
||||
}
|
||||
let t33;
|
||||
if ($[51] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t33 = getNewlineInstructions();
|
||||
$[51] = t33;
|
||||
} else {
|
||||
t33 = $[51];
|
||||
}
|
||||
let t34;
|
||||
if ($[52] !== dimColor) {
|
||||
t34 = <Box><Text dimColor={dimColor}>{t33}</Text></Box>;
|
||||
$[52] = dimColor;
|
||||
$[53] = t34;
|
||||
} else {
|
||||
t34 = $[53];
|
||||
}
|
||||
let t35;
|
||||
if ($[54] !== t28 || $[55] !== t29 || $[56] !== t30 || $[57] !== t31 || $[58] !== t32 || $[59] !== t34 || $[60] !== terminalShortcutElement) {
|
||||
t35 = <Box flexDirection="column" width={t28}>{t29}{t30}{t31}{t32}{terminalShortcutElement}{t34}</Box>;
|
||||
$[54] = t28;
|
||||
$[55] = t29;
|
||||
$[56] = t30;
|
||||
$[57] = t31;
|
||||
$[58] = t32;
|
||||
$[59] = t34;
|
||||
$[60] = terminalShortcutElement;
|
||||
$[61] = t35;
|
||||
} else {
|
||||
t35 = $[61];
|
||||
}
|
||||
let t36;
|
||||
if ($[62] !== dimColor || $[63] !== undoShortcut) {
|
||||
t36 = <Box><Text dimColor={dimColor}>{undoShortcut} to undo</Text></Box>;
|
||||
$[62] = dimColor;
|
||||
$[63] = undoShortcut;
|
||||
$[64] = t36;
|
||||
} else {
|
||||
t36 = $[64];
|
||||
}
|
||||
let t37;
|
||||
if ($[65] !== dimColor) {
|
||||
t37 = getPlatform() !== "windows" && <Box><Text dimColor={dimColor}>ctrl + z to suspend</Text></Box>;
|
||||
$[65] = dimColor;
|
||||
$[66] = t37;
|
||||
} else {
|
||||
t37 = $[66];
|
||||
}
|
||||
let t38;
|
||||
if ($[67] !== dimColor || $[68] !== imagePasteShortcut) {
|
||||
t38 = <Box><Text dimColor={dimColor}>{imagePasteShortcut} to paste images</Text></Box>;
|
||||
$[67] = dimColor;
|
||||
$[68] = imagePasteShortcut;
|
||||
$[69] = t38;
|
||||
} else {
|
||||
t38 = $[69];
|
||||
}
|
||||
let t39;
|
||||
if ($[70] !== dimColor || $[71] !== modelPickerShortcut) {
|
||||
t39 = <Box><Text dimColor={dimColor}>{modelPickerShortcut} to switch model</Text></Box>;
|
||||
$[70] = dimColor;
|
||||
$[71] = modelPickerShortcut;
|
||||
$[72] = t39;
|
||||
} else {
|
||||
t39 = $[72];
|
||||
}
|
||||
let t40;
|
||||
if ($[73] !== dimColor || $[74] !== fastModeShortcut) {
|
||||
t40 = isFastModeEnabled() && isFastModeAvailable() && <Box><Text dimColor={dimColor}>{fastModeShortcut} to toggle fast mode</Text></Box>;
|
||||
$[73] = dimColor;
|
||||
$[74] = fastModeShortcut;
|
||||
$[75] = t40;
|
||||
} else {
|
||||
t40 = $[75];
|
||||
}
|
||||
let t41;
|
||||
if ($[76] !== dimColor || $[77] !== stashShortcut) {
|
||||
t41 = <Box><Text dimColor={dimColor}>{stashShortcut} to stash prompt</Text></Box>;
|
||||
$[76] = dimColor;
|
||||
$[77] = stashShortcut;
|
||||
$[78] = t41;
|
||||
} else {
|
||||
t41 = $[78];
|
||||
}
|
||||
let t42;
|
||||
if ($[79] !== dimColor || $[80] !== externalEditorShortcut) {
|
||||
t42 = <Box><Text dimColor={dimColor}>{externalEditorShortcut} to edit in $EDITOR</Text></Box>;
|
||||
$[79] = dimColor;
|
||||
$[80] = externalEditorShortcut;
|
||||
$[81] = t42;
|
||||
} else {
|
||||
t42 = $[81];
|
||||
}
|
||||
let t43;
|
||||
if ($[82] !== dimColor) {
|
||||
t43 = isKeybindingCustomizationEnabled() && <Box><Text dimColor={dimColor}>/keybindings to customize</Text></Box>;
|
||||
$[82] = dimColor;
|
||||
$[83] = t43;
|
||||
} else {
|
||||
t43 = $[83];
|
||||
}
|
||||
let t44;
|
||||
if ($[84] !== t36 || $[85] !== t37 || $[86] !== t38 || $[87] !== t39 || $[88] !== t40 || $[89] !== t41 || $[90] !== t42 || $[91] !== t43) {
|
||||
t44 = <Box flexDirection="column">{t36}{t37}{t38}{t39}{t40}{t41}{t42}{t43}</Box>;
|
||||
$[84] = t36;
|
||||
$[85] = t37;
|
||||
$[86] = t38;
|
||||
$[87] = t39;
|
||||
$[88] = t40;
|
||||
$[89] = t41;
|
||||
$[90] = t42;
|
||||
$[91] = t43;
|
||||
$[92] = t44;
|
||||
} else {
|
||||
t44 = $[92];
|
||||
}
|
||||
let t45;
|
||||
if ($[93] !== gap || $[94] !== paddingX || $[95] !== t27 || $[96] !== t35 || $[97] !== t44) {
|
||||
t45 = <Box paddingX={paddingX} flexDirection="row" gap={gap}>{t27}{t35}{t44}</Box>;
|
||||
$[93] = gap;
|
||||
$[94] = paddingX;
|
||||
$[95] = t27;
|
||||
$[96] = t35;
|
||||
$[97] = t44;
|
||||
$[98] = t45;
|
||||
} else {
|
||||
t45 = $[98];
|
||||
}
|
||||
return t45;
|
||||
dimColor?: boolean
|
||||
fixedWidth?: boolean
|
||||
gap?: number
|
||||
paddingX?: number
|
||||
}
|
||||
|
||||
export function PromptInputHelpMenu(props: Props): React.ReactNode {
|
||||
const { dimColor, fixedWidth, gap, paddingX } = props
|
||||
|
||||
// Get configured shortcuts from keybinding system
|
||||
const transcriptShortcut = formatShortcut(
|
||||
useShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o'),
|
||||
)
|
||||
const todosShortcut = formatShortcut(
|
||||
useShortcutDisplay('app:toggleTodos', 'Global', 'ctrl+t'),
|
||||
)
|
||||
const undoShortcut = formatShortcut(
|
||||
useShortcutDisplay('chat:undo', 'Chat', 'ctrl+_'),
|
||||
)
|
||||
const stashShortcut = formatShortcut(
|
||||
useShortcutDisplay('chat:stash', 'Chat', 'ctrl+s'),
|
||||
)
|
||||
const cycleModeShortcut = formatShortcut(
|
||||
useShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab'),
|
||||
)
|
||||
const modelPickerShortcut = formatShortcut(
|
||||
useShortcutDisplay('chat:modelPicker', 'Chat', 'alt+p'),
|
||||
)
|
||||
const fastModeShortcut = formatShortcut(
|
||||
useShortcutDisplay('chat:fastMode', 'Chat', 'alt+o'),
|
||||
)
|
||||
const externalEditorShortcut = formatShortcut(
|
||||
useShortcutDisplay('chat:externalEditor', 'Chat', 'ctrl+g'),
|
||||
)
|
||||
const terminalShortcut = formatShortcut(
|
||||
useShortcutDisplay('app:toggleTerminal', 'Global', 'meta+j'),
|
||||
)
|
||||
const imagePasteShortcut = formatShortcut(
|
||||
useShortcutDisplay('chat:imagePaste', 'Chat', 'ctrl+v'),
|
||||
)
|
||||
|
||||
// Compute terminal shortcut element outside JSX to satisfy feature() constraint
|
||||
const terminalShortcutElement = feature('TERMINAL_PANEL') ? (
|
||||
getFeatureValue_CACHED_MAY_BE_STALE('tengu_terminal_panel', false) ? (
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>{terminalShortcut} for terminal</Text>
|
||||
</Box>
|
||||
) : null
|
||||
) : null
|
||||
|
||||
return (
|
||||
<Box paddingX={paddingX} flexDirection="row" gap={gap}>
|
||||
<Box flexDirection="column" width={fixedWidth ? 24 : undefined}>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>! for bash mode</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>/ for commands</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>@ for file paths</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>& for background</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>/btw for side question</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexDirection="column" width={fixedWidth ? 35 : undefined}>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>double tap esc to clear input</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>
|
||||
{cycleModeShortcut}{' '}
|
||||
{process.env.USER_TYPE === 'ant'
|
||||
? 'to cycle modes'
|
||||
: 'to auto-accept edits'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>
|
||||
{transcriptShortcut} for verbose output
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>{todosShortcut} to toggle tasks</Text>
|
||||
</Box>
|
||||
{terminalShortcutElement}
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>{getNewlineInstructions()}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>{undoShortcut} to undo</Text>
|
||||
</Box>
|
||||
{getPlatform() !== 'windows' && (
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>ctrl + z to suspend</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>{imagePasteShortcut} to paste images</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>{modelPickerShortcut} to switch model</Text>
|
||||
</Box>
|
||||
{isFastModeEnabled() && isFastModeAvailable() && (
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>
|
||||
{fastModeShortcut} to toggle fast mode
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>{stashShortcut} to stash prompt</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>
|
||||
{externalEditorShortcut} to edit in $EDITOR
|
||||
</Text>
|
||||
</Box>
|
||||
{isKeybindingCustomizationEnabled() && (
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>/keybindings to customize</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from 'src/ink.js';
|
||||
import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from 'src/tools/AgentTool/agentColorManager.js';
|
||||
import type { PromptInputMode } from 'src/types/textInputTypes.js';
|
||||
import { getTeammateColor } from 'src/utils/teammate.js';
|
||||
import type { Theme } from 'src/utils/theme.js';
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from 'src/ink.js'
|
||||
import {
|
||||
AGENT_COLOR_TO_THEME_COLOR,
|
||||
AGENT_COLORS,
|
||||
type AgentColorName,
|
||||
} from 'src/tools/AgentTool/agentColorManager.js'
|
||||
import type { PromptInputMode } from 'src/types/textInputTypes.js'
|
||||
import { getTeammateColor } from 'src/utils/teammate.js'
|
||||
import type { Theme } from 'src/utils/theme.js'
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
|
||||
|
||||
type Props = {
|
||||
mode: PromptInputMode;
|
||||
isLoading: boolean;
|
||||
viewingAgentName?: string;
|
||||
viewingAgentColor?: AgentColorName;
|
||||
};
|
||||
mode: PromptInputMode
|
||||
isLoading: boolean
|
||||
viewingAgentName?: string
|
||||
viewingAgentColor?: AgentColorName
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the theme color key for the teammate's assigned color.
|
||||
@@ -20,73 +24,81 @@ type Props = {
|
||||
*/
|
||||
function getTeammateThemeColor(): keyof Theme | undefined {
|
||||
if (!isAgentSwarmsEnabled()) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
const colorName = getTeammateColor();
|
||||
const colorName = getTeammateColor()
|
||||
if (!colorName) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
if (AGENT_COLORS.includes(colorName as AgentColorName)) {
|
||||
return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName];
|
||||
return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]
|
||||
}
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
type PromptCharProps = {
|
||||
isLoading: boolean;
|
||||
isLoading: boolean
|
||||
// Dead code elimination: parameter named themeColor to avoid "teammate" string in external builds
|
||||
themeColor?: keyof Theme;
|
||||
};
|
||||
themeColor?: keyof Theme
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the prompt character (❯).
|
||||
* Teammate color overrides the default color when set.
|
||||
*/
|
||||
function PromptChar(t0) {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
isLoading,
|
||||
themeColor
|
||||
} = t0;
|
||||
const teammateColor = themeColor;
|
||||
const color = teammateColor ?? (false ? "subtle" : undefined);
|
||||
let t1;
|
||||
if ($[0] !== color || $[1] !== isLoading) {
|
||||
t1 = <Text color={color} dimColor={isLoading}>{figures.pointer} </Text>;
|
||||
$[0] = color;
|
||||
$[1] = isLoading;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
function PromptChar({
|
||||
isLoading,
|
||||
themeColor,
|
||||
}: PromptCharProps): React.ReactNode {
|
||||
// Assign to original name for clarity within the function
|
||||
const teammateColor = themeColor
|
||||
const isAnt = process.env.USER_TYPE === 'ant'
|
||||
const color = teammateColor ?? (isAnt ? 'subtle' : undefined)
|
||||
|
||||
return (
|
||||
<Text color={color} dimColor={isLoading}>
|
||||
{figures.pointer}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
export function PromptInputModeIndicator(t0) {
|
||||
const $ = _c(6);
|
||||
const {
|
||||
mode,
|
||||
isLoading,
|
||||
viewingAgentName,
|
||||
viewingAgentColor
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = getTeammateThemeColor();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const teammateColor = t1;
|
||||
const viewedTeammateThemeColor = viewingAgentColor ? AGENT_COLOR_TO_THEME_COLOR[viewingAgentColor] : undefined;
|
||||
let t2;
|
||||
if ($[1] !== isLoading || $[2] !== mode || $[3] !== viewedTeammateThemeColor || $[4] !== viewingAgentName) {
|
||||
t2 = <Box alignItems="flex-start" alignSelf="flex-start" flexWrap="nowrap" justifyContent="flex-start">{viewingAgentName ? <PromptChar isLoading={isLoading} themeColor={viewedTeammateThemeColor} /> : mode === "bash" ? <Text color="bashBorder" dimColor={isLoading}>! </Text> : <PromptChar isLoading={isLoading} themeColor={isAgentSwarmsEnabled() ? teammateColor : undefined} />}</Box>;
|
||||
$[1] = isLoading;
|
||||
$[2] = mode;
|
||||
$[3] = viewedTeammateThemeColor;
|
||||
$[4] = viewingAgentName;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
return t2;
|
||||
|
||||
export function PromptInputModeIndicator({
|
||||
mode,
|
||||
isLoading,
|
||||
viewingAgentName,
|
||||
viewingAgentColor,
|
||||
}: Props): React.ReactNode {
|
||||
const teammateColor = getTeammateThemeColor()
|
||||
|
||||
// Convert viewed teammate's color to theme color
|
||||
// Falls back to PromptChar's default (subtle for ants, undefined for external)
|
||||
const viewedTeammateThemeColor = viewingAgentColor
|
||||
? AGENT_COLOR_TO_THEME_COLOR[viewingAgentColor]
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Box
|
||||
alignItems="flex-start"
|
||||
alignSelf="flex-start"
|
||||
flexWrap="nowrap"
|
||||
justifyContent="flex-start"
|
||||
>
|
||||
{viewingAgentName ? (
|
||||
// Use teammate's color on the standard prompt character, matching established style
|
||||
<PromptChar
|
||||
isLoading={isLoading}
|
||||
themeColor={viewedTeammateThemeColor}
|
||||
/>
|
||||
) : mode === 'bash' ? (
|
||||
<Text color="bashBorder" dimColor={isLoading}>
|
||||
!
|
||||
</Text>
|
||||
) : (
|
||||
<PromptChar
|
||||
isLoading={isLoading}
|
||||
themeColor={isAgentSwarmsEnabled() ? teammateColor : undefined}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Box } from 'src/ink.js';
|
||||
import { useAppState } from 'src/state/AppState.js';
|
||||
import { STATUS_TAG, SUMMARY_TAG, TASK_NOTIFICATION_TAG } from '../../constants/xml.js';
|
||||
import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js';
|
||||
import { useCommandQueue } from '../../hooks/useCommandQueue.js';
|
||||
import type { QueuedCommand } from '../../types/textInputTypes.js';
|
||||
import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js';
|
||||
import { createUserMessage, EMPTY_LOOKUPS, normalizeMessages } from '../../utils/messages.js';
|
||||
import { jsonParse } from '../../utils/slowOperations.js';
|
||||
import { Message } from '../Message.js';
|
||||
const EMPTY_SET = new Set<string>();
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { Box } from 'src/ink.js'
|
||||
import { useAppState } from 'src/state/AppState.js'
|
||||
import {
|
||||
STATUS_TAG,
|
||||
SUMMARY_TAG,
|
||||
TASK_NOTIFICATION_TAG,
|
||||
} from '../../constants/xml.js'
|
||||
import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js'
|
||||
import { useCommandQueue } from '../../hooks/useCommandQueue.js'
|
||||
import type { QueuedCommand } from '../../types/textInputTypes.js'
|
||||
import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js'
|
||||
import {
|
||||
createUserMessage,
|
||||
EMPTY_LOOKUPS,
|
||||
normalizeMessages,
|
||||
} from '../../utils/messages.js'
|
||||
import { jsonParse } from '../../utils/slowOperations.js'
|
||||
import { Message } from '../Message.js'
|
||||
|
||||
const EMPTY_SET = new Set<string>()
|
||||
|
||||
/**
|
||||
* Check if a command value is an idle notification that should be hidden.
|
||||
@@ -19,15 +28,15 @@ const EMPTY_SET = new Set<string>();
|
||||
*/
|
||||
function isIdleNotification(value: string): boolean {
|
||||
try {
|
||||
const parsed = jsonParse(value);
|
||||
return parsed?.type === 'idle_notification';
|
||||
const parsed = jsonParse(value)
|
||||
return parsed?.type === 'idle_notification'
|
||||
} catch {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Maximum number of task notification lines to show
|
||||
const MAX_VISIBLE_NOTIFICATIONS = 3;
|
||||
const MAX_VISIBLE_NOTIFICATIONS = 3
|
||||
|
||||
/**
|
||||
* Create a synthetic overflow notification message for capped task notifications.
|
||||
@@ -36,7 +45,7 @@ function createOverflowNotificationMessage(count: number): string {
|
||||
return `<${TASK_NOTIFICATION_TAG}>
|
||||
<${SUMMARY_TAG}>+${count} more tasks completed</${SUMMARY_TAG}>
|
||||
<${STATUS_TAG}>completed</${STATUS_TAG}>
|
||||
</${TASK_NOTIFICATION_TAG}>`;
|
||||
</${TASK_NOTIFICATION_TAG}>`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,73 +53,114 @@ function createOverflowNotificationMessage(count: number): string {
|
||||
* Other command types are always shown in full.
|
||||
* Idle notifications are filtered out entirely.
|
||||
*/
|
||||
function processQueuedCommands(queuedCommands: QueuedCommand[]): QueuedCommand[] {
|
||||
function processQueuedCommands(
|
||||
queuedCommands: QueuedCommand[],
|
||||
): QueuedCommand[] {
|
||||
// Filter out idle notifications - they are processed silently
|
||||
const filteredCommands = queuedCommands.filter(cmd => typeof cmd.value !== 'string' || !isIdleNotification(cmd.value));
|
||||
const filteredCommands = queuedCommands.filter(
|
||||
cmd => typeof cmd.value !== 'string' || !isIdleNotification(cmd.value),
|
||||
)
|
||||
|
||||
// Separate task notifications from other commands
|
||||
const taskNotifications = filteredCommands.filter(cmd => cmd.mode === 'task-notification');
|
||||
const otherCommands = filteredCommands.filter(cmd => cmd.mode !== 'task-notification');
|
||||
const taskNotifications = filteredCommands.filter(
|
||||
cmd => cmd.mode === 'task-notification',
|
||||
)
|
||||
const otherCommands = filteredCommands.filter(
|
||||
cmd => cmd.mode !== 'task-notification',
|
||||
)
|
||||
|
||||
// If notifications fit within limit, return all commands as-is
|
||||
if (taskNotifications.length <= MAX_VISIBLE_NOTIFICATIONS) {
|
||||
return [...otherCommands, ...taskNotifications];
|
||||
return [...otherCommands, ...taskNotifications]
|
||||
}
|
||||
|
||||
// Show first (MAX_VISIBLE_NOTIFICATIONS - 1) notifications, then a summary
|
||||
const visibleNotifications = taskNotifications.slice(0, MAX_VISIBLE_NOTIFICATIONS - 1);
|
||||
const overflowCount = taskNotifications.length - (MAX_VISIBLE_NOTIFICATIONS - 1);
|
||||
const visibleNotifications = taskNotifications.slice(
|
||||
0,
|
||||
MAX_VISIBLE_NOTIFICATIONS - 1,
|
||||
)
|
||||
const overflowCount =
|
||||
taskNotifications.length - (MAX_VISIBLE_NOTIFICATIONS - 1)
|
||||
|
||||
// Create synthetic overflow message
|
||||
const overflowCommand: QueuedCommand = {
|
||||
value: createOverflowNotificationMessage(overflowCount),
|
||||
mode: 'task-notification'
|
||||
};
|
||||
return [...otherCommands, ...visibleNotifications, overflowCommand];
|
||||
mode: 'task-notification',
|
||||
}
|
||||
|
||||
return [...otherCommands, ...visibleNotifications, overflowCommand]
|
||||
}
|
||||
|
||||
function PromptInputQueuedCommandsImpl(): React.ReactNode {
|
||||
const queuedCommands = useCommandQueue();
|
||||
const viewingAgent = useAppState(s => !!s.viewingAgentTaskId);
|
||||
const queuedCommands = useCommandQueue()
|
||||
const viewingAgent = useAppState(s => !!s.viewingAgentTaskId)
|
||||
// Brief layout: dim queue items + skip the paddingX (brief messages
|
||||
// already indent themselves). Gate mirrors the brief-spinner/message
|
||||
// check elsewhere — no teammate-view override needed since this
|
||||
// component early-returns when viewing a teammate.
|
||||
const useBriefLayout = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s_0 => s_0.isBriefOnly) : false;
|
||||
const useBriefLayout =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s => s.isBriefOnly)
|
||||
: false
|
||||
|
||||
// createUserMessage mints a fresh UUID per call; without memoization, streaming
|
||||
// re-renders defeat Message's areMessagePropsEqual (compares uuid) → flicker.
|
||||
const messages = useMemo(() => {
|
||||
if (queuedCommands.length === 0) return null;
|
||||
if (queuedCommands.length === 0) return null
|
||||
// task-notification is shown via useInboxNotification; most isMeta commands
|
||||
// (scheduled tasks, proactive ticks) are system-generated and hidden.
|
||||
// Channel messages are the exception — isMeta but shown so the keyboard
|
||||
// user sees what arrived.
|
||||
const visibleCommands = queuedCommands.filter(isQueuedCommandVisible);
|
||||
if (visibleCommands.length === 0) return null;
|
||||
const processedCommands = processQueuedCommands(visibleCommands);
|
||||
return normalizeMessages(processedCommands.map(cmd => {
|
||||
let content = cmd.value;
|
||||
if (cmd.mode === 'bash' && typeof content === 'string') {
|
||||
content = `<bash-input>${content}</bash-input>`;
|
||||
}
|
||||
// [Image #N] placeholders are inline in the text value (inserted at
|
||||
// paste time), so the queue preview shows them without stub blocks.
|
||||
return createUserMessage({
|
||||
content
|
||||
});
|
||||
}));
|
||||
}, [queuedCommands]);
|
||||
const visibleCommands = queuedCommands.filter(isQueuedCommandVisible)
|
||||
if (visibleCommands.length === 0) return null
|
||||
const processedCommands = processQueuedCommands(visibleCommands)
|
||||
return normalizeMessages(
|
||||
processedCommands.map(cmd => {
|
||||
let content = cmd.value
|
||||
if (cmd.mode === 'bash' && typeof content === 'string') {
|
||||
content = `<bash-input>${content}</bash-input>`
|
||||
}
|
||||
// [Image #N] placeholders are inline in the text value (inserted at
|
||||
// paste time), so the queue preview shows them without stub blocks.
|
||||
return createUserMessage({ content })
|
||||
}),
|
||||
)
|
||||
}, [queuedCommands])
|
||||
|
||||
// Don't show leader's queued commands when viewing any agent's transcript
|
||||
if (viewingAgent || messages === null) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return <Box marginTop={1} flexDirection="column">
|
||||
{messages.map((message, i) => <QueuedMessageProvider key={i} isFirst={i === 0} useBriefLayout={useBriefLayout}>
|
||||
<Message message={message} lookups={EMPTY_LOOKUPS} addMargin={false} tools={[]} commands={[]} verbose={false} inProgressToolUseIDs={EMPTY_SET} progressMessagesForMessage={[]} shouldAnimate={false} shouldShowDot={false} isTranscriptMode={false} isStatic={true} />
|
||||
</QueuedMessageProvider>)}
|
||||
</Box>;
|
||||
|
||||
return (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{messages.map((message, i) => (
|
||||
<QueuedMessageProvider
|
||||
key={i}
|
||||
isFirst={i === 0}
|
||||
useBriefLayout={useBriefLayout}
|
||||
>
|
||||
<Message
|
||||
message={message}
|
||||
lookups={EMPTY_LOOKUPS}
|
||||
addMargin={false}
|
||||
tools={[]}
|
||||
commands={[]}
|
||||
verbose={false}
|
||||
inProgressToolUseIDs={EMPTY_SET}
|
||||
progressMessagesForMessage={[]}
|
||||
shouldAnimate={false}
|
||||
shouldShowDot={false}
|
||||
isTranscriptMode={false}
|
||||
isStatic={true}
|
||||
/>
|
||||
</QueuedMessageProvider>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
export const PromptInputQueuedCommands = React.memo(PromptInputQueuedCommandsImpl);
|
||||
|
||||
export const PromptInputQueuedCommands = React.memo(
|
||||
PromptInputQueuedCommandsImpl,
|
||||
)
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from 'src/ink.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from 'src/ink.js'
|
||||
|
||||
type Props = {
|
||||
hasStash: boolean;
|
||||
};
|
||||
export function PromptInputStashNotice(t0) {
|
||||
const $ = _c(1);
|
||||
const {
|
||||
hasStash
|
||||
} = t0;
|
||||
if (!hasStash) {
|
||||
return null;
|
||||
}
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box paddingLeft={2}><Text dimColor={true}>{figures.pointerSmall} Stashed (auto-restores after submit)</Text></Box>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
return t1;
|
||||
hasStash: boolean
|
||||
}
|
||||
|
||||
export function PromptInputStashNotice({ hasStash }: Props): React.ReactNode {
|
||||
if (!hasStash) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box paddingLeft={2}>
|
||||
<Text dimColor>
|
||||
{figures.pointerSmall} Stashed (auto-restores after submit)
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,63 +1,61 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
|
||||
export function SandboxPromptFooterHint() {
|
||||
const $ = _c(6);
|
||||
const [recentViolationCount, setRecentViolationCount] = useState(0);
|
||||
const timerRef = useRef(null);
|
||||
const detailsShortcut = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
if (!SandboxManager.isSandboxingEnabled()) {
|
||||
return;
|
||||
}
|
||||
const store = SandboxManager.getSandboxViolationStore();
|
||||
let lastCount = store.getTotalCount();
|
||||
const unsubscribe = store.subscribe(() => {
|
||||
const currentCount = store.getTotalCount();
|
||||
const newViolations = currentCount - lastCount;
|
||||
if (newViolations > 0) {
|
||||
setRecentViolationCount(newViolations);
|
||||
lastCount = currentCount;
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
timerRef.current = setTimeout(setRecentViolationCount, 5000, 0);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
import * as React from 'react'
|
||||
import { type ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
|
||||
export function SandboxPromptFooterHint(): ReactNode {
|
||||
const [recentViolationCount, setRecentViolationCount] = useState(0)
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const detailsShortcut = useShortcutDisplay(
|
||||
'app:toggleTranscript',
|
||||
'Global',
|
||||
'ctrl+o',
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!SandboxManager.isSandboxingEnabled()) {
|
||||
return
|
||||
}
|
||||
|
||||
const store = SandboxManager.getSandboxViolationStore()
|
||||
let lastCount = store.getTotalCount()
|
||||
|
||||
const unsubscribe = store.subscribe(() => {
|
||||
const currentCount = store.getTotalCount()
|
||||
const newViolations = currentCount - lastCount
|
||||
|
||||
if (newViolations > 0) {
|
||||
setRecentViolationCount(newViolations)
|
||||
lastCount = currentCount
|
||||
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
clearTimeout(timerRef.current)
|
||||
}
|
||||
};
|
||||
};
|
||||
t1 = [];
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t1 = $[1];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
|
||||
timerRef.current = setTimeout(setRecentViolationCount, 5000, 0)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!SandboxManager.isSandboxingEnabled() || recentViolationCount === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const t2 = recentViolationCount === 1 ? "operation" : "operations";
|
||||
let t3;
|
||||
if ($[2] !== detailsShortcut || $[3] !== recentViolationCount || $[4] !== t2) {
|
||||
t3 = <Box paddingX={0} paddingY={0}><Text color="inactive" wrap="truncate">⧈ Sandbox blocked {recentViolationCount}{" "}{t2} ·{" "}{detailsShortcut} for details · /sandbox to disable</Text></Box>;
|
||||
$[2] = detailsShortcut;
|
||||
$[3] = recentViolationCount;
|
||||
$[4] = t2;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
return t3;
|
||||
|
||||
return (
|
||||
<Box paddingX={0} paddingY={0}>
|
||||
<Text color="inactive" wrap="truncate">
|
||||
⧈ Sandbox blocked {recentViolationCount}{' '}
|
||||
{recentViolationCount === 1 ? 'operation' : 'operations'} ·{' '}
|
||||
{detailsShortcut} for details · /sandbox to disable
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,142 +1,121 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Ansi, Box, Text, useAnimationFrame } from '../../ink.js';
|
||||
import { segmentTextByHighlights, type TextHighlight } from '../../utils/textHighlighting.js';
|
||||
import { ShimmerChar } from '../Spinner/ShimmerChar.js';
|
||||
import * as React from 'react'
|
||||
import { Ansi, Box, Text, useAnimationFrame } from '../../ink.js'
|
||||
import {
|
||||
segmentTextByHighlights,
|
||||
type TextHighlight,
|
||||
} from '../../utils/textHighlighting.js'
|
||||
import { ShimmerChar } from '../Spinner/ShimmerChar.js'
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
highlights: TextHighlight[];
|
||||
};
|
||||
text: string
|
||||
highlights: TextHighlight[]
|
||||
}
|
||||
|
||||
type LinePart = {
|
||||
text: string;
|
||||
highlight: TextHighlight | undefined;
|
||||
start: number;
|
||||
};
|
||||
export function HighlightedInput(t0) {
|
||||
const $ = _c(23);
|
||||
const {
|
||||
text,
|
||||
highlights
|
||||
} = t0;
|
||||
let lines;
|
||||
if ($[0] !== highlights || $[1] !== text) {
|
||||
const segments = segmentTextByHighlights(text, highlights);
|
||||
lines = [[]];
|
||||
let pos = 0;
|
||||
text: string
|
||||
highlight: TextHighlight | undefined
|
||||
start: number
|
||||
}
|
||||
|
||||
export function HighlightedInput({ text, highlights }: Props): React.ReactNode {
|
||||
// The shimmer animation (below) re-renders this component at 20fps while the
|
||||
// ultrathink keyword is present. text/highlights are referentially stable
|
||||
// across animation ticks (parent doesn't re-render), so memoize everything
|
||||
// that derives from them: segmentTextByHighlights alone is ~85µs/call
|
||||
// (tokenize + sort + O(n²) overlap), which adds up fast at 20fps.
|
||||
const { lines, hasShimmer, sweepStart, cycleLength } = React.useMemo(() => {
|
||||
const segments = segmentTextByHighlights(text, highlights)
|
||||
|
||||
// Split segments by newlines into per-line groups. Ink's row-direction Box
|
||||
// indents continuation lines of a multi-line child to that child's X offset.
|
||||
// By splitting at newlines, each line renders as its own row, avoiding the
|
||||
// incorrect indentation when highlighted text is followed by wrapped content.
|
||||
const lines: LinePart[][] = [[]]
|
||||
let pos = 0
|
||||
for (const segment of segments) {
|
||||
const parts = segment.text.split("\n");
|
||||
const parts = segment.text.split('\n')
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i > 0) {
|
||||
lines.push([]);
|
||||
pos = pos + 1;
|
||||
lines.push([])
|
||||
pos += 1
|
||||
}
|
||||
const part = parts[i];
|
||||
const part = parts[i]!
|
||||
if (part.length > 0) {
|
||||
lines[lines.length - 1].push({
|
||||
lines[lines.length - 1]!.push({
|
||||
text: part,
|
||||
highlight: segment.highlight,
|
||||
start: pos
|
||||
});
|
||||
start: pos,
|
||||
})
|
||||
}
|
||||
pos = pos + part.length;
|
||||
pos += part.length
|
||||
}
|
||||
}
|
||||
$[0] = highlights;
|
||||
$[1] = text;
|
||||
$[2] = lines;
|
||||
} else {
|
||||
lines = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== highlights) {
|
||||
t1 = highlights.some(_temp);
|
||||
$[3] = highlights;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
const hasShimmer = t1;
|
||||
let sweepStart = 0;
|
||||
let cycleLength = 1;
|
||||
if (hasShimmer) {
|
||||
let lo = Infinity;
|
||||
let hi = -Infinity;
|
||||
if ($[5] !== hi || $[6] !== highlights || $[7] !== lo) {
|
||||
for (const h_0 of highlights) {
|
||||
if (h_0.shimmerColor) {
|
||||
lo = Math.min(lo, h_0.start);
|
||||
hi = Math.max(hi, h_0.end);
|
||||
|
||||
// Scope the sweep to shimmer-highlighted ranges so cycle time doesn't grow
|
||||
// with input length. Padding creates an offscreen pause between sweeps.
|
||||
const hasShimmer = highlights.some(h => h.shimmerColor)
|
||||
let sweepStart = 0
|
||||
let cycleLength = 1
|
||||
if (hasShimmer) {
|
||||
const padding = 10
|
||||
let lo = Infinity
|
||||
let hi = -Infinity
|
||||
for (const h of highlights) {
|
||||
if (h.shimmerColor) {
|
||||
lo = Math.min(lo, h.start)
|
||||
hi = Math.max(hi, h.end)
|
||||
}
|
||||
}
|
||||
$[5] = hi;
|
||||
$[6] = highlights;
|
||||
$[7] = lo;
|
||||
$[8] = lo;
|
||||
$[9] = hi;
|
||||
} else {
|
||||
lo = $[8] as number;
|
||||
hi = $[9] as number;
|
||||
sweepStart = lo - padding
|
||||
cycleLength = hi - lo + padding * 2
|
||||
}
|
||||
sweepStart = lo - 10;
|
||||
cycleLength = hi - lo + 20;
|
||||
}
|
||||
let t2;
|
||||
if ($[10] !== cycleLength || $[11] !== hasShimmer || $[12] !== lines || $[13] !== sweepStart) {
|
||||
t2 = {
|
||||
lines,
|
||||
hasShimmer,
|
||||
sweepStart,
|
||||
cycleLength
|
||||
};
|
||||
$[10] = cycleLength;
|
||||
$[11] = hasShimmer;
|
||||
$[12] = lines;
|
||||
$[13] = sweepStart;
|
||||
$[14] = t2;
|
||||
} else {
|
||||
t2 = $[14];
|
||||
}
|
||||
const {
|
||||
lines: lines_0,
|
||||
hasShimmer: hasShimmer_0,
|
||||
sweepStart: sweepStart_0,
|
||||
cycleLength: cycleLength_0
|
||||
} = t2;
|
||||
const [ref, time] = useAnimationFrame(hasShimmer_0 ? 50 : null);
|
||||
const glimmerIndex = hasShimmer_0 ? sweepStart_0 + Math.floor(time / 50) % cycleLength_0 : -100;
|
||||
let t3;
|
||||
if ($[15] !== glimmerIndex || $[16] !== lines_0) {
|
||||
let t4;
|
||||
if ($[18] !== glimmerIndex) {
|
||||
t4 = (lineParts, lineIndex) => <Box key={lineIndex}>{lineParts.length === 0 ? <Text> </Text> : lineParts.map((part_0, partIndex) => {
|
||||
if (part_0.highlight?.shimmerColor && part_0.highlight.color) {
|
||||
return <Text key={partIndex}>{part_0.text.split("").map((char, charIndex) => <ShimmerChar key={charIndex} char={char} index={part_0.start + charIndex} glimmerIndex={glimmerIndex} messageColor={part_0.highlight.color} shimmerColor={part_0.highlight.shimmerColor} />)}</Text>;
|
||||
}
|
||||
return <Text key={partIndex} color={part_0.highlight?.color} dimColor={part_0.highlight?.dimColor} inverse={part_0.highlight?.inverse}><Ansi>{part_0.text}</Ansi></Text>;
|
||||
})}</Box>;
|
||||
$[18] = glimmerIndex;
|
||||
$[19] = t4;
|
||||
} else {
|
||||
t4 = $[19];
|
||||
}
|
||||
t3 = lines_0.map(t4);
|
||||
$[15] = glimmerIndex;
|
||||
$[16] = lines_0;
|
||||
$[17] = t3;
|
||||
} else {
|
||||
t3 = $[17];
|
||||
}
|
||||
let t4;
|
||||
if ($[20] !== ref || $[21] !== t3) {
|
||||
t4 = <Box ref={ref} flexDirection="column">{t3}</Box>;
|
||||
$[20] = ref;
|
||||
$[21] = t3;
|
||||
$[22] = t4;
|
||||
} else {
|
||||
t4 = $[22];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
function _temp(h) {
|
||||
return h.shimmerColor;
|
||||
|
||||
return { lines, hasShimmer, sweepStart, cycleLength }
|
||||
}, [text, highlights])
|
||||
|
||||
const [ref, time] = useAnimationFrame(hasShimmer ? 50 : null)
|
||||
const glimmerIndex = hasShimmer
|
||||
? sweepStart + (Math.floor(time / 50) % cycleLength)
|
||||
: -100
|
||||
|
||||
return (
|
||||
<Box ref={ref} flexDirection="column">
|
||||
{lines.map((lineParts, lineIndex) => (
|
||||
<Box key={lineIndex}>
|
||||
{lineParts.length === 0 ? (
|
||||
<Text> </Text>
|
||||
) : (
|
||||
lineParts.map((part, partIndex) => {
|
||||
if (part.highlight?.shimmerColor && part.highlight.color) {
|
||||
return (
|
||||
<Text key={partIndex}>
|
||||
{part.text.split('').map((char, charIndex) => (
|
||||
<ShimmerChar
|
||||
key={charIndex}
|
||||
char={char}
|
||||
index={part.start + charIndex}
|
||||
glimmerIndex={glimmerIndex}
|
||||
messageColor={part.highlight!.color!}
|
||||
shimmerColor={part.highlight!.shimmerColor!}
|
||||
/>
|
||||
))}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Text
|
||||
key={partIndex}
|
||||
color={part.highlight?.color}
|
||||
dimColor={part.highlight?.dimColor}
|
||||
inverse={part.highlight?.inverse}
|
||||
>
|
||||
<Ansi>{part.text}</Ansi>
|
||||
</Text>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,73 +1,32 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import { useSettings } from '../../hooks/useSettings.js';
|
||||
import { Box, Text, useAnimationFrame } from '../../ink.js';
|
||||
import { interpolateColor, toRGBColor } from '../Spinner/utils.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import { useSettings } from '../../hooks/useSettings.js'
|
||||
import { Box, Text, useAnimationFrame } from '../../ink.js'
|
||||
import { interpolateColor, toRGBColor } from '../Spinner/utils.js'
|
||||
|
||||
type Props = {
|
||||
voiceState: 'idle' | 'recording' | 'processing';
|
||||
};
|
||||
voiceState: 'idle' | 'recording' | 'processing'
|
||||
}
|
||||
|
||||
// Processing shimmer colors: dim gray to lighter gray (matches ThinkingShimmerText)
|
||||
const PROCESSING_DIM = {
|
||||
r: 153,
|
||||
g: 153,
|
||||
b: 153
|
||||
};
|
||||
const PROCESSING_BRIGHT = {
|
||||
r: 185,
|
||||
g: 185,
|
||||
b: 185
|
||||
};
|
||||
const PULSE_PERIOD_S = 2; // 2 second period for all pulsing animations
|
||||
const PROCESSING_DIM = { r: 153, g: 153, b: 153 }
|
||||
const PROCESSING_BRIGHT = { r: 185, g: 185, b: 185 }
|
||||
|
||||
export function VoiceIndicator(props) {
|
||||
const $ = _c(2);
|
||||
if (!feature("VOICE_MODE")) {
|
||||
return null;
|
||||
}
|
||||
let t0;
|
||||
if ($[0] !== props) {
|
||||
t0 = <VoiceIndicatorImpl {...props} />;
|
||||
$[0] = props;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
const PULSE_PERIOD_S = 2 // 2 second period for all pulsing animations
|
||||
|
||||
export function VoiceIndicator(props: Props): React.ReactNode {
|
||||
if (!feature('VOICE_MODE')) return null
|
||||
return <VoiceIndicatorImpl {...props} />
|
||||
}
|
||||
function VoiceIndicatorImpl(t0) {
|
||||
const $ = _c(2);
|
||||
const {
|
||||
voiceState
|
||||
} = t0;
|
||||
|
||||
function VoiceIndicatorImpl({ voiceState }: Props): React.ReactNode {
|
||||
switch (voiceState) {
|
||||
case "recording":
|
||||
{
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text dimColor={true}>listening…</Text>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case "processing":
|
||||
{
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <ProcessingShimmer />;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case "idle":
|
||||
{
|
||||
return null;
|
||||
}
|
||||
case 'recording':
|
||||
return <Text dimColor>listening…</Text>
|
||||
case 'processing':
|
||||
return <ProcessingShimmer />
|
||||
case 'idle':
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,62 +34,30 @@ function VoiceIndicatorImpl(t0) {
|
||||
// is too brief for a 1s-period shimmer to register, and a 50ms animation
|
||||
// timer here runs concurrently with auto-repeat spaces arriving every
|
||||
// 30-80ms, compounding re-renders during an already-busy window.
|
||||
export function VoiceWarmupHint() {
|
||||
const $ = _c(1);
|
||||
if (!feature("VOICE_MODE")) {
|
||||
return null;
|
||||
}
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <Text dimColor={true}>keep holding…</Text>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
export function VoiceWarmupHint(): React.ReactNode {
|
||||
if (!feature('VOICE_MODE')) return null
|
||||
return <Text dimColor>keep holding…</Text>
|
||||
}
|
||||
function ProcessingShimmer() {
|
||||
const $ = _c(8);
|
||||
const settings = useSettings();
|
||||
const reducedMotion = settings.prefersReducedMotion ?? false;
|
||||
const [ref, time] = useAnimationFrame(reducedMotion ? null : 50);
|
||||
|
||||
function ProcessingShimmer(): React.ReactNode {
|
||||
const settings = useSettings()
|
||||
const reducedMotion = settings.prefersReducedMotion ?? false
|
||||
const [ref, time] = useAnimationFrame(reducedMotion ? null : 50)
|
||||
|
||||
if (reducedMotion) {
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <Text color="warning">Voice: processing…</Text>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
return <Text color="warning">Voice: processing…</Text>
|
||||
}
|
||||
const elapsedSec = time / 1000;
|
||||
const opacity = (Math.sin(elapsedSec * Math.PI * 2 / PULSE_PERIOD_S) + 1) / 2;
|
||||
let t0;
|
||||
if ($[1] !== opacity) {
|
||||
t0 = toRGBColor(interpolateColor(PROCESSING_DIM, PROCESSING_BRIGHT, opacity));
|
||||
$[1] = opacity;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
const color = t0;
|
||||
let t1;
|
||||
if ($[3] !== color) {
|
||||
t1 = <Text color={color}>Voice: processing…</Text>;
|
||||
$[3] = color;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
let t2;
|
||||
if ($[5] !== ref || $[6] !== t1) {
|
||||
t2 = <Box ref={ref}>{t1}</Box>;
|
||||
$[5] = ref;
|
||||
$[6] = t1;
|
||||
$[7] = t2;
|
||||
} else {
|
||||
t2 = $[7];
|
||||
}
|
||||
return t2;
|
||||
|
||||
const elapsedSec = time / 1000
|
||||
const opacity =
|
||||
(Math.sin((elapsedSec * Math.PI * 2) / PULSE_PERIOD_S) + 1) / 2
|
||||
const color = toRGBColor(
|
||||
interpolateColor(PROCESSING_DIM, PROCESSING_BRIGHT, opacity),
|
||||
)
|
||||
|
||||
return (
|
||||
<Box ref={ref}>
|
||||
<Text color={color}>Voice: processing…</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,219 +1,148 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import type { Tools } from '../../Tool.js';
|
||||
import { getAgentColor } from '../../tools/AgentTool/agentColorManager.js';
|
||||
import { getMemoryScopeDisplay } from '../../tools/AgentTool/agentMemory.js';
|
||||
import { resolveAgentTools } from '../../tools/AgentTool/agentToolUtils.js';
|
||||
import { type AgentDefinition, isBuiltInAgent } from '../../tools/AgentTool/loadAgentsDir.js';
|
||||
import { getAgentModelDisplay } from '../../utils/model/agent.js';
|
||||
import { Markdown } from '../Markdown.js';
|
||||
import { getActualRelativeAgentFilePath } from './agentFileUtils.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import type { Tools } from '../../Tool.js'
|
||||
import { getAgentColor } from '../../tools/AgentTool/agentColorManager.js'
|
||||
import { getMemoryScopeDisplay } from '../../tools/AgentTool/agentMemory.js'
|
||||
import { resolveAgentTools } from '../../tools/AgentTool/agentToolUtils.js'
|
||||
import {
|
||||
type AgentDefinition,
|
||||
isBuiltInAgent,
|
||||
} from '../../tools/AgentTool/loadAgentsDir.js'
|
||||
import { getAgentModelDisplay } from '../../utils/model/agent.js'
|
||||
import { Markdown } from '../Markdown.js'
|
||||
import { getActualRelativeAgentFilePath } from './agentFileUtils.js'
|
||||
|
||||
type Props = {
|
||||
agent: AgentDefinition;
|
||||
tools: Tools;
|
||||
allAgents?: AgentDefinition[];
|
||||
onBack: () => void;
|
||||
};
|
||||
export function AgentDetail(t0) {
|
||||
const $ = _c(48);
|
||||
const {
|
||||
agent,
|
||||
tools,
|
||||
onBack
|
||||
} = t0;
|
||||
const resolvedTools = resolveAgentTools(agent, tools, false);
|
||||
let t1;
|
||||
if ($[0] !== agent) {
|
||||
t1 = getActualRelativeAgentFilePath(agent);
|
||||
$[0] = agent;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const filePath = t1;
|
||||
let t2;
|
||||
if ($[2] !== agent.agentType) {
|
||||
t2 = getAgentColor(agent.agentType);
|
||||
$[2] = agent.agentType;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const backgroundColor = t2;
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
useKeybinding("confirm:no", onBack, t3);
|
||||
let t4;
|
||||
if ($[5] !== onBack) {
|
||||
t4 = e => {
|
||||
if (e.key === "return") {
|
||||
e.preventDefault();
|
||||
onBack();
|
||||
}
|
||||
};
|
||||
$[5] = onBack;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
const handleKeyDown = t4;
|
||||
const renderToolsList = function renderToolsList() {
|
||||
if (resolvedTools.hasWildcard) {
|
||||
return <Text>All tools</Text>;
|
||||
}
|
||||
if (!agent.tools || agent.tools.length === 0) {
|
||||
return <Text>None</Text>;
|
||||
}
|
||||
return <>{resolvedTools.validTools.length > 0 && <Text>{resolvedTools.validTools.join(", ")}</Text>}{resolvedTools.invalidTools.length > 0 && <Text color="warning">{figures.warning} Unrecognized:{" "}{resolvedTools.invalidTools.join(", ")}</Text>}</>;
|
||||
};
|
||||
const T0 = Box;
|
||||
const t5 = "column";
|
||||
const t6 = 1;
|
||||
const t7 = 0;
|
||||
const t8 = true;
|
||||
let t9;
|
||||
if ($[7] !== filePath) {
|
||||
t9 = <Text dimColor={true}>{filePath}</Text>;
|
||||
$[7] = filePath;
|
||||
$[8] = t9;
|
||||
} else {
|
||||
t9 = $[8];
|
||||
}
|
||||
let t10;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = <Text><Text bold={true}>Description</Text> (tells Claude when to use this agent):</Text>;
|
||||
$[9] = t10;
|
||||
} else {
|
||||
t10 = $[9];
|
||||
}
|
||||
let t11;
|
||||
if ($[10] !== agent.whenToUse) {
|
||||
t11 = <Box flexDirection="column">{t10}<Box marginLeft={2}><Text>{agent.whenToUse}</Text></Box></Box>;
|
||||
$[10] = agent.whenToUse;
|
||||
$[11] = t11;
|
||||
} else {
|
||||
t11 = $[11];
|
||||
}
|
||||
const T1 = Box;
|
||||
let t12;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t12 = <Text><Text bold={true}>Tools</Text>:{" "}</Text>;
|
||||
$[12] = t12;
|
||||
} else {
|
||||
t12 = $[12];
|
||||
}
|
||||
const t13 = renderToolsList();
|
||||
let t14;
|
||||
if ($[13] !== T1 || $[14] !== t12 || $[15] !== t13) {
|
||||
t14 = <T1>{t12}{t13}</T1>;
|
||||
$[13] = T1;
|
||||
$[14] = t12;
|
||||
$[15] = t13;
|
||||
$[16] = t14;
|
||||
} else {
|
||||
t14 = $[16];
|
||||
}
|
||||
let t15;
|
||||
if ($[17] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t15 = <Text bold={true}>Model</Text>;
|
||||
$[17] = t15;
|
||||
} else {
|
||||
t15 = $[17];
|
||||
}
|
||||
let t16;
|
||||
if ($[18] !== agent.model) {
|
||||
t16 = getAgentModelDisplay(agent.model);
|
||||
$[18] = agent.model;
|
||||
$[19] = t16;
|
||||
} else {
|
||||
t16 = $[19];
|
||||
}
|
||||
let t17;
|
||||
if ($[20] !== t16) {
|
||||
t17 = <Text>{t15}: {t16}</Text>;
|
||||
$[20] = t16;
|
||||
$[21] = t17;
|
||||
} else {
|
||||
t17 = $[21];
|
||||
}
|
||||
let t18;
|
||||
if ($[22] !== agent.permissionMode) {
|
||||
t18 = agent.permissionMode && <Text><Text bold={true}>Permission mode</Text>: {agent.permissionMode}</Text>;
|
||||
$[22] = agent.permissionMode;
|
||||
$[23] = t18;
|
||||
} else {
|
||||
t18 = $[23];
|
||||
}
|
||||
let t19;
|
||||
if ($[24] !== agent.memory) {
|
||||
t19 = agent.memory && <Text><Text bold={true}>Memory</Text>: {getMemoryScopeDisplay(agent.memory)}</Text>;
|
||||
$[24] = agent.memory;
|
||||
$[25] = t19;
|
||||
} else {
|
||||
t19 = $[25];
|
||||
}
|
||||
let t20;
|
||||
if ($[26] !== agent.hooks) {
|
||||
t20 = agent.hooks && Object.keys(agent.hooks).length > 0 && <Text><Text bold={true}>Hooks</Text>: {Object.keys(agent.hooks).join(", ")}</Text>;
|
||||
$[26] = agent.hooks;
|
||||
$[27] = t20;
|
||||
} else {
|
||||
t20 = $[27];
|
||||
}
|
||||
let t21;
|
||||
if ($[28] !== agent.skills) {
|
||||
t21 = agent.skills && agent.skills.length > 0 && <Text><Text bold={true}>Skills</Text>:{" "}{agent.skills.length > 10 ? `${agent.skills.length} skills` : agent.skills.join(", ")}</Text>;
|
||||
$[28] = agent.skills;
|
||||
$[29] = t21;
|
||||
} else {
|
||||
t21 = $[29];
|
||||
}
|
||||
let t22;
|
||||
if ($[30] !== agent.agentType || $[31] !== backgroundColor) {
|
||||
t22 = backgroundColor && <Box><Text><Text bold={true}>Color</Text>:{" "}<Text backgroundColor={backgroundColor} color="inverseText">{" "}{agent.agentType}{" "}</Text></Text></Box>;
|
||||
$[30] = agent.agentType;
|
||||
$[31] = backgroundColor;
|
||||
$[32] = t22;
|
||||
} else {
|
||||
t22 = $[32];
|
||||
}
|
||||
let t23;
|
||||
if ($[33] !== agent) {
|
||||
t23 = !isBuiltInAgent(agent) && <><Box><Text><Text bold={true}>System prompt</Text>:</Text></Box><Box marginLeft={2} marginRight={2}><Markdown>{agent.getSystemPrompt()}</Markdown></Box></>;
|
||||
$[33] = agent;
|
||||
$[34] = t23;
|
||||
} else {
|
||||
t23 = $[34];
|
||||
}
|
||||
let t24;
|
||||
if ($[35] !== T0 || $[36] !== handleKeyDown || $[37] !== t11 || $[38] !== t14 || $[39] !== t17 || $[40] !== t18 || $[41] !== t19 || $[42] !== t20 || $[43] !== t21 || $[44] !== t22 || $[45] !== t23 || $[46] !== t9) {
|
||||
t24 = <T0 flexDirection={t5} gap={t6} tabIndex={t7} autoFocus={t8} onKeyDown={handleKeyDown}>{t9}{t11}{t14}{t17}{t18}{t19}{t20}{t21}{t22}{t23}</T0>;
|
||||
$[35] = T0;
|
||||
$[36] = handleKeyDown;
|
||||
$[37] = t11;
|
||||
$[38] = t14;
|
||||
$[39] = t17;
|
||||
$[40] = t18;
|
||||
$[41] = t19;
|
||||
$[42] = t20;
|
||||
$[43] = t21;
|
||||
$[44] = t22;
|
||||
$[45] = t23;
|
||||
$[46] = t9;
|
||||
$[47] = t24;
|
||||
} else {
|
||||
t24 = $[47];
|
||||
}
|
||||
return t24;
|
||||
agent: AgentDefinition
|
||||
tools: Tools
|
||||
allAgents?: AgentDefinition[]
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
export function AgentDetail({ agent, tools, onBack }: Props): React.ReactNode {
|
||||
const resolvedTools = resolveAgentTools(agent, tools, false)
|
||||
const filePath = getActualRelativeAgentFilePath(agent)
|
||||
const backgroundColor = getAgentColor(agent.agentType)
|
||||
|
||||
// Handle Esc to go back
|
||||
useKeybinding('confirm:no', onBack, { context: 'Confirmation' })
|
||||
|
||||
// Handle Enter to go back
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'return') {
|
||||
e.preventDefault()
|
||||
onBack()
|
||||
}
|
||||
}
|
||||
|
||||
function renderToolsList(): React.ReactNode {
|
||||
if (resolvedTools.hasWildcard) {
|
||||
return <Text>All tools</Text>
|
||||
}
|
||||
|
||||
if (!agent.tools || agent.tools.length === 0) {
|
||||
return <Text>None</Text>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{resolvedTools.validTools.length > 0 && (
|
||||
<Text>{resolvedTools.validTools.join(', ')}</Text>
|
||||
)}
|
||||
{resolvedTools.invalidTools.length > 0 && (
|
||||
<Text color="warning">
|
||||
{figures.warning} Unrecognized:{' '}
|
||||
{resolvedTools.invalidTools.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Text dimColor>{filePath}</Text>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
<Text bold>Description</Text> (tells Claude when to use this agent):
|
||||
</Text>
|
||||
<Box marginLeft={2}>
|
||||
<Text>{agent.whenToUse}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text>
|
||||
<Text bold>Tools</Text>:{' '}
|
||||
</Text>
|
||||
{renderToolsList()}
|
||||
</Box>
|
||||
|
||||
<Text>
|
||||
<Text bold>Model</Text>: {getAgentModelDisplay(agent.model)}
|
||||
</Text>
|
||||
|
||||
{agent.permissionMode && (
|
||||
<Text>
|
||||
<Text bold>Permission mode</Text>: {agent.permissionMode}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{agent.memory && (
|
||||
<Text>
|
||||
<Text bold>Memory</Text>: {getMemoryScopeDisplay(agent.memory)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{agent.hooks && Object.keys(agent.hooks).length > 0 && (
|
||||
<Text>
|
||||
<Text bold>Hooks</Text>: {Object.keys(agent.hooks).join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{agent.skills && agent.skills.length > 0 && (
|
||||
<Text>
|
||||
<Text bold>Skills</Text>:{' '}
|
||||
{agent.skills.length > 10
|
||||
? `${agent.skills.length} skills`
|
||||
: agent.skills.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{backgroundColor && (
|
||||
<Box>
|
||||
<Text>
|
||||
<Text bold>Color</Text>:{' '}
|
||||
<Text backgroundColor={backgroundColor} color="inverseText">
|
||||
{' '}
|
||||
{agent.agentType}{' '}
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!isBuiltInAgent(agent) && (
|
||||
<>
|
||||
<Box>
|
||||
<Text>
|
||||
<Text bold>System prompt</Text>:
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginLeft={2} marginRight={2}>
|
||||
<Markdown>{agent.getSystemPrompt()}</Markdown>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,177 +1,246 @@
|
||||
import chalk from 'chalk';
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSetAppState } from 'src/state/AppState.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import type { Tools } from '../../Tool.js';
|
||||
import { type AgentColorName, setAgentColor } from '../../tools/AgentTool/agentColorManager.js';
|
||||
import { type AgentDefinition, getActiveAgentsFromList, isCustomAgent, isPluginAgent } from '../../tools/AgentTool/loadAgentsDir.js';
|
||||
import { editFileInEditor } from '../../utils/promptEditor.js';
|
||||
import { getActualAgentFilePath, updateAgentFile } from './agentFileUtils.js';
|
||||
import { ColorPicker } from './ColorPicker.js';
|
||||
import { ModelSelector } from './ModelSelector.js';
|
||||
import { ToolSelector } from './ToolSelector.js';
|
||||
import { getAgentSourceDisplayName } from './utils.js';
|
||||
import chalk from 'chalk'
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useSetAppState } from 'src/state/AppState.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import type { Tools } from '../../Tool.js'
|
||||
import {
|
||||
type AgentColorName,
|
||||
setAgentColor,
|
||||
} from '../../tools/AgentTool/agentColorManager.js'
|
||||
import {
|
||||
type AgentDefinition,
|
||||
getActiveAgentsFromList,
|
||||
isCustomAgent,
|
||||
isPluginAgent,
|
||||
} from '../../tools/AgentTool/loadAgentsDir.js'
|
||||
import { editFileInEditor } from '../../utils/promptEditor.js'
|
||||
import { getActualAgentFilePath, updateAgentFile } from './agentFileUtils.js'
|
||||
import { ColorPicker } from './ColorPicker.js'
|
||||
import { ModelSelector } from './ModelSelector.js'
|
||||
import { ToolSelector } from './ToolSelector.js'
|
||||
import { getAgentSourceDisplayName } from './utils.js'
|
||||
|
||||
type Props = {
|
||||
agent: AgentDefinition;
|
||||
tools: Tools;
|
||||
onSaved: (message: string) => void;
|
||||
onBack: () => void;
|
||||
};
|
||||
type EditMode = 'menu' | 'edit-tools' | 'edit-color' | 'edit-model';
|
||||
agent: AgentDefinition
|
||||
tools: Tools
|
||||
onSaved: (message: string) => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
type EditMode = 'menu' | 'edit-tools' | 'edit-color' | 'edit-model'
|
||||
|
||||
type SaveChanges = {
|
||||
tools?: string[];
|
||||
color?: AgentColorName;
|
||||
model?: string;
|
||||
};
|
||||
tools?: string[]
|
||||
color?: AgentColorName
|
||||
model?: string
|
||||
}
|
||||
|
||||
export function AgentEditor({
|
||||
agent,
|
||||
tools,
|
||||
onSaved,
|
||||
onBack
|
||||
onBack,
|
||||
}: Props): React.ReactNode {
|
||||
const setAppState = useSetAppState();
|
||||
const [editMode, setEditMode] = useState<EditMode>('menu');
|
||||
const [selectedMenuIndex, setSelectedMenuIndex] = useState(0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedColor, setSelectedColor] = useState<AgentColorName | undefined>(agent.color as AgentColorName | undefined);
|
||||
const setAppState = useSetAppState()
|
||||
const [editMode, setEditMode] = useState<EditMode>('menu')
|
||||
const [selectedMenuIndex, setSelectedMenuIndex] = useState(0)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [selectedColor, setSelectedColor] = useState<
|
||||
AgentColorName | undefined
|
||||
>(agent.color as AgentColorName | undefined)
|
||||
|
||||
const handleOpenInEditor = useCallback(async () => {
|
||||
const filePath = getActualAgentFilePath(agent);
|
||||
const result = await editFileInEditor(filePath);
|
||||
const filePath = getActualAgentFilePath(agent)
|
||||
const result = await editFileInEditor(filePath)
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error);
|
||||
setError(result.error)
|
||||
} else {
|
||||
onSaved(`Opened ${agent.agentType} in editor. If you made edits, restart to load the latest version.`);
|
||||
onSaved(
|
||||
`Opened ${agent.agentType} in editor. If you made edits, restart to load the latest version.`,
|
||||
)
|
||||
}
|
||||
}, [agent, onSaved]);
|
||||
const handleSave = useCallback(async (changes: SaveChanges = {}) => {
|
||||
const {
|
||||
tools: newTools,
|
||||
color: newColor,
|
||||
model: newModel
|
||||
} = changes;
|
||||
const finalColor = newColor ?? selectedColor;
|
||||
const hasToolsChanged = newTools !== undefined;
|
||||
const hasModelChanged = newModel !== undefined;
|
||||
const hasColorChanged = finalColor !== agent.color;
|
||||
if (!hasToolsChanged && !hasModelChanged && !hasColorChanged) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// Only custom/plugin agents can be edited
|
||||
// this is for type safety; the UI shouldn't allow editing otherwise
|
||||
if (!isCustomAgent(agent) && !isPluginAgent(agent)) {
|
||||
return false;
|
||||
}, [agent, onSaved])
|
||||
|
||||
const handleSave = useCallback(
|
||||
async (changes: SaveChanges = {}) => {
|
||||
const { tools: newTools, color: newColor, model: newModel } = changes
|
||||
const finalColor = newColor ?? selectedColor
|
||||
const hasToolsChanged = newTools !== undefined
|
||||
const hasModelChanged = newModel !== undefined
|
||||
const hasColorChanged = finalColor !== agent.color
|
||||
|
||||
if (!hasToolsChanged && !hasModelChanged && !hasColorChanged) {
|
||||
return false
|
||||
}
|
||||
await updateAgentFile(agent, agent.whenToUse, newTools ?? agent.tools, agent.getSystemPrompt(), finalColor, newModel ?? agent.model);
|
||||
if (hasColorChanged && finalColor) {
|
||||
setAgentColor(agent.agentType, finalColor);
|
||||
}
|
||||
setAppState(state => {
|
||||
const allAgents = state.agentDefinitions.allAgents.map(a => a.agentType === agent.agentType ? {
|
||||
...a,
|
||||
tools: newTools ?? a.tools,
|
||||
color: finalColor,
|
||||
model: newModel ?? a.model
|
||||
} : a);
|
||||
return {
|
||||
...state,
|
||||
agentDefinitions: {
|
||||
...state.agentDefinitions,
|
||||
activeAgents: getActiveAgentsFromList(allAgents),
|
||||
allAgents
|
||||
|
||||
try {
|
||||
// Only custom/plugin agents can be edited
|
||||
// this is for type safety; the UI shouldn't allow editing otherwise
|
||||
if (!isCustomAgent(agent) && !isPluginAgent(agent)) {
|
||||
return false
|
||||
}
|
||||
|
||||
await updateAgentFile(
|
||||
agent,
|
||||
agent.whenToUse,
|
||||
newTools ?? agent.tools,
|
||||
agent.getSystemPrompt(),
|
||||
finalColor,
|
||||
newModel ?? agent.model,
|
||||
)
|
||||
|
||||
if (hasColorChanged && finalColor) {
|
||||
setAgentColor(agent.agentType, finalColor)
|
||||
}
|
||||
|
||||
setAppState(state => {
|
||||
const allAgents = state.agentDefinitions.allAgents.map(a =>
|
||||
a.agentType === agent.agentType
|
||||
? {
|
||||
...a,
|
||||
tools: newTools ?? a.tools,
|
||||
color: finalColor,
|
||||
model: newModel ?? a.model,
|
||||
}
|
||||
: a,
|
||||
)
|
||||
return {
|
||||
...state,
|
||||
agentDefinitions: {
|
||||
...state.agentDefinitions,
|
||||
activeAgents: getActiveAgentsFromList(allAgents),
|
||||
allAgents,
|
||||
},
|
||||
}
|
||||
};
|
||||
});
|
||||
onSaved(`Updated agent: ${chalk.bold(agent.agentType)}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to save agent');
|
||||
return false;
|
||||
}
|
||||
}, [agent, selectedColor, onSaved, setAppState]);
|
||||
const menuItems = useMemo(() => [{
|
||||
label: 'Open in editor',
|
||||
action: handleOpenInEditor
|
||||
}, {
|
||||
label: 'Edit tools',
|
||||
action: () => setEditMode('edit-tools')
|
||||
}, {
|
||||
label: 'Edit model',
|
||||
action: () => setEditMode('edit-model')
|
||||
}, {
|
||||
label: 'Edit color',
|
||||
action: () => setEditMode('edit-color')
|
||||
}], [handleOpenInEditor]);
|
||||
const handleEscape = useCallback(() => {
|
||||
setError(null);
|
||||
if (editMode === 'menu') {
|
||||
onBack();
|
||||
} else {
|
||||
setEditMode('menu');
|
||||
}
|
||||
}, [editMode, onBack]);
|
||||
const handleMenuKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === 'up') {
|
||||
e.preventDefault();
|
||||
setSelectedMenuIndex(index => Math.max(0, index - 1));
|
||||
} else if (e.key === 'down') {
|
||||
e.preventDefault();
|
||||
setSelectedMenuIndex(index_0 => Math.min(menuItems.length - 1, index_0 + 1));
|
||||
} else if (e.key === 'return') {
|
||||
e.preventDefault();
|
||||
const selectedItem = menuItems[selectedMenuIndex];
|
||||
if (selectedItem) {
|
||||
void selectedItem.action();
|
||||
})
|
||||
|
||||
onSaved(`Updated agent: ${chalk.bold(agent.agentType)}`)
|
||||
return true
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to save agent')
|
||||
return false
|
||||
}
|
||||
},
|
||||
[agent, selectedColor, onSaved, setAppState],
|
||||
)
|
||||
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
{ label: 'Open in editor', action: handleOpenInEditor },
|
||||
{ label: 'Edit tools', action: () => setEditMode('edit-tools') },
|
||||
{ label: 'Edit model', action: () => setEditMode('edit-model') },
|
||||
{ label: 'Edit color', action: () => setEditMode('edit-color') },
|
||||
],
|
||||
[handleOpenInEditor],
|
||||
)
|
||||
|
||||
const handleEscape = useCallback(() => {
|
||||
setError(null)
|
||||
if (editMode === 'menu') {
|
||||
onBack()
|
||||
} else {
|
||||
setEditMode('menu')
|
||||
}
|
||||
}, [menuItems, selectedMenuIndex]);
|
||||
useKeybinding('confirm:no', handleEscape, {
|
||||
context: 'Confirmation'
|
||||
});
|
||||
const renderMenu = (): React.ReactNode => <Box flexDirection="column" tabIndex={0} autoFocus onKeyDown={handleMenuKeyDown}>
|
||||
}, [editMode, onBack])
|
||||
|
||||
const handleMenuKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === 'up') {
|
||||
e.preventDefault()
|
||||
setSelectedMenuIndex(index => Math.max(0, index - 1))
|
||||
} else if (e.key === 'down') {
|
||||
e.preventDefault()
|
||||
setSelectedMenuIndex(index => Math.min(menuItems.length - 1, index + 1))
|
||||
} else if (e.key === 'return') {
|
||||
e.preventDefault()
|
||||
const selectedItem = menuItems[selectedMenuIndex]
|
||||
if (selectedItem) {
|
||||
void selectedItem.action()
|
||||
}
|
||||
}
|
||||
},
|
||||
[menuItems, selectedMenuIndex],
|
||||
)
|
||||
|
||||
useKeybinding('confirm:no', handleEscape, { context: 'Confirmation' })
|
||||
|
||||
const renderMenu = (): React.ReactNode => (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleMenuKeyDown}
|
||||
>
|
||||
<Text dimColor>Source: {getAgentSourceDisplayName(agent.source)}</Text>
|
||||
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{menuItems.map((item, index_1) => <Text key={item.label} color={index_1 === selectedMenuIndex ? 'suggestion' : undefined}>
|
||||
{index_1 === selectedMenuIndex ? `${figures.pointer} ` : ' '}
|
||||
{menuItems.map((item, index) => (
|
||||
<Text
|
||||
key={item.label}
|
||||
color={index === selectedMenuIndex ? 'suggestion' : undefined}
|
||||
>
|
||||
{index === selectedMenuIndex ? `${figures.pointer} ` : ' '}
|
||||
{item.label}
|
||||
</Text>)}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{error && <Box marginTop={1}>
|
||||
{error && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="error">{error}</Text>
|
||||
</Box>}
|
||||
</Box>;
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
|
||||
switch (editMode) {
|
||||
case 'menu':
|
||||
return renderMenu();
|
||||
return renderMenu()
|
||||
|
||||
case 'edit-tools':
|
||||
return <ToolSelector tools={tools} initialTools={agent.tools} onComplete={async finalTools => {
|
||||
setEditMode('menu');
|
||||
await handleSave({
|
||||
tools: finalTools
|
||||
});
|
||||
}} />;
|
||||
return (
|
||||
<ToolSelector
|
||||
tools={tools}
|
||||
initialTools={agent.tools}
|
||||
onComplete={async finalTools => {
|
||||
setEditMode('menu')
|
||||
await handleSave({ tools: finalTools })
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'edit-color':
|
||||
return <ColorPicker agentName={agent.agentType} currentColor={selectedColor || agent.color as AgentColorName || 'automatic'} onConfirm={async color => {
|
||||
setSelectedColor(color);
|
||||
setEditMode('menu');
|
||||
await handleSave({
|
||||
color
|
||||
});
|
||||
}} />;
|
||||
return (
|
||||
<ColorPicker
|
||||
agentName={agent.agentType}
|
||||
currentColor={
|
||||
selectedColor || (agent.color as AgentColorName) || 'automatic'
|
||||
}
|
||||
onConfirm={async color => {
|
||||
setSelectedColor(color)
|
||||
setEditMode('menu')
|
||||
await handleSave({ color })
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'edit-model':
|
||||
return <ModelSelector initialModel={agent.model} onComplete={async model => {
|
||||
setEditMode('menu');
|
||||
await handleSave({
|
||||
model
|
||||
});
|
||||
}} />;
|
||||
return (
|
||||
<ModelSelector
|
||||
initialModel={agent.model}
|
||||
onComplete={async model => {
|
||||
setEditMode('menu')
|
||||
await handleSave({ model })
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
default:
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import * as React from 'react'
|
||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
type Props = {
|
||||
instructions?: string;
|
||||
};
|
||||
export function AgentNavigationFooter(t0) {
|
||||
const $ = _c(2);
|
||||
const {
|
||||
instructions: t1
|
||||
} = t0;
|
||||
const instructions = t1 === undefined ? "Press \u2191\u2193 to navigate \xB7 Enter to select \xB7 Esc to go back" : t1;
|
||||
const exitState = useExitOnCtrlCDWithKeybindings();
|
||||
const t2 = exitState.pending ? `Press ${exitState.keyName} again to exit` : instructions;
|
||||
let t3;
|
||||
if ($[0] !== t2) {
|
||||
t3 = <Box marginLeft={2}><Text dimColor={true}>{t2}</Text></Box>;
|
||||
$[0] = t2;
|
||||
$[1] = t3;
|
||||
} else {
|
||||
t3 = $[1];
|
||||
}
|
||||
return t3;
|
||||
instructions?: string
|
||||
}
|
||||
|
||||
export function AgentNavigationFooter({
|
||||
instructions = 'Press ↑↓ to navigate · Enter to select · Esc to go back',
|
||||
}: Props): React.ReactNode {
|
||||
const exitState = useExitOnCtrlCDWithKeybindings()
|
||||
|
||||
return (
|
||||
<Box marginLeft={2}>
|
||||
<Text dimColor>
|
||||
{exitState.pending
|
||||
? `Press ${exitState.keyName} again to exit`
|
||||
: instructions}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,439 +1,342 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import type { SettingSource } from 'src/utils/settings/constants.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { ResolvedAgent } from '../../tools/AgentTool/agentDisplay.js';
|
||||
import { AGENT_SOURCE_GROUPS, compareAgentsByName, getOverrideSourceLabel, resolveAgentModelDisplay } from '../../tools/AgentTool/agentDisplay.js';
|
||||
import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js';
|
||||
import { count } from '../../utils/array.js';
|
||||
import { Dialog } from '../design-system/Dialog.js';
|
||||
import { Divider } from '../design-system/Divider.js';
|
||||
import { getAgentSourceDisplayName } from './utils.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import type { SettingSource } from 'src/utils/settings/constants.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { ResolvedAgent } from '../../tools/AgentTool/agentDisplay.js'
|
||||
import {
|
||||
AGENT_SOURCE_GROUPS,
|
||||
compareAgentsByName,
|
||||
getOverrideSourceLabel,
|
||||
resolveAgentModelDisplay,
|
||||
} from '../../tools/AgentTool/agentDisplay.js'
|
||||
import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js'
|
||||
import { count } from '../../utils/array.js'
|
||||
import { Dialog } from '../design-system/Dialog.js'
|
||||
import { Divider } from '../design-system/Divider.js'
|
||||
import { getAgentSourceDisplayName } from './utils.js'
|
||||
|
||||
type Props = {
|
||||
source: SettingSource | 'all' | 'built-in' | 'plugin';
|
||||
agents: ResolvedAgent[];
|
||||
onBack: () => void;
|
||||
onSelect: (agent: AgentDefinition) => void;
|
||||
onCreateNew?: () => void;
|
||||
changes?: string[];
|
||||
};
|
||||
export function AgentsList(t0) {
|
||||
const $ = _c(96);
|
||||
const {
|
||||
source,
|
||||
agents,
|
||||
onBack,
|
||||
onSelect,
|
||||
onCreateNew,
|
||||
changes
|
||||
} = t0;
|
||||
const [selectedAgent, setSelectedAgent] = React.useState(null);
|
||||
const [isCreateNewSelected, setIsCreateNewSelected] = React.useState(true);
|
||||
let t1;
|
||||
if ($[0] !== agents) {
|
||||
t1 = [...agents].sort(compareAgentsByName);
|
||||
$[0] = agents;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const sortedAgents = t1;
|
||||
const getOverrideInfo = _temp;
|
||||
let t2;
|
||||
if ($[2] !== isCreateNewSelected) {
|
||||
t2 = () => <Box><Text color={isCreateNewSelected ? "suggestion" : undefined}>{isCreateNewSelected ? `${figures.pointer} ` : " "}</Text><Text color={isCreateNewSelected ? "suggestion" : undefined}>Create new agent</Text></Box>;
|
||||
$[2] = isCreateNewSelected;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const renderCreateNewOption = t2;
|
||||
let t3;
|
||||
if ($[4] !== isCreateNewSelected || $[5] !== selectedAgent?.agentType || $[6] !== selectedAgent?.source) {
|
||||
t3 = agent_0 => {
|
||||
const isBuiltIn = agent_0.source === "built-in";
|
||||
const isSelected = !isBuiltIn && !isCreateNewSelected && selectedAgent?.agentType === agent_0.agentType && selectedAgent?.source === agent_0.source;
|
||||
const {
|
||||
isOverridden,
|
||||
overriddenBy
|
||||
} = getOverrideInfo(agent_0);
|
||||
const dimmed = isBuiltIn || isOverridden;
|
||||
const textColor = !isBuiltIn && isSelected ? "suggestion" : undefined;
|
||||
const resolvedModel = resolveAgentModelDisplay(agent_0);
|
||||
return <Box key={`${agent_0.agentType}-${agent_0.source}`}><Text dimColor={dimmed && !isSelected} color={textColor}>{isBuiltIn ? "" : isSelected ? `${figures.pointer} ` : " "}</Text><Text dimColor={dimmed && !isSelected} color={textColor}>{agent_0.agentType}</Text>{resolvedModel && <Text dimColor={true} color={textColor}>{" \xB7 "}{resolvedModel}</Text>}{agent_0.memory && <Text dimColor={true} color={textColor}>{" \xB7 "}{agent_0.memory} memory</Text>}{overriddenBy && <Text dimColor={!isSelected} color={isSelected ? "warning" : undefined}>{" "}{figures.warning} shadowed by {getOverrideSourceLabel(overriddenBy)}</Text>}</Box>;
|
||||
};
|
||||
$[4] = isCreateNewSelected;
|
||||
$[5] = selectedAgent?.agentType;
|
||||
$[6] = selectedAgent?.source;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
const renderAgent = t3;
|
||||
let t4;
|
||||
if ($[8] !== sortedAgents || $[9] !== source) {
|
||||
bb0: {
|
||||
const nonBuiltIn = sortedAgents.filter(_temp2);
|
||||
if (source === "all") {
|
||||
t4 = AGENT_SOURCE_GROUPS.filter(_temp3).flatMap(t5 => {
|
||||
const {
|
||||
source: groupSource
|
||||
} = t5;
|
||||
return nonBuiltIn.filter(a_0 => a_0.source === groupSource);
|
||||
});
|
||||
break bb0;
|
||||
}
|
||||
t4 = nonBuiltIn;
|
||||
source: SettingSource | 'all' | 'built-in' | 'plugin'
|
||||
agents: ResolvedAgent[]
|
||||
onBack: () => void
|
||||
onSelect: (agent: AgentDefinition) => void
|
||||
onCreateNew?: () => void
|
||||
changes?: string[]
|
||||
}
|
||||
|
||||
export function AgentsList({
|
||||
source,
|
||||
agents,
|
||||
onBack,
|
||||
onSelect,
|
||||
onCreateNew,
|
||||
changes,
|
||||
}: Props): React.ReactNode {
|
||||
const [selectedAgent, setSelectedAgent] =
|
||||
React.useState<ResolvedAgent | null>(null)
|
||||
const [isCreateNewSelected, setIsCreateNewSelected] = React.useState(true)
|
||||
|
||||
// Sort agents alphabetically by name within each source group
|
||||
const sortedAgents = React.useMemo(
|
||||
() => [...agents].sort(compareAgentsByName),
|
||||
[agents],
|
||||
)
|
||||
|
||||
const getOverrideInfo = (agent: ResolvedAgent) => {
|
||||
return {
|
||||
isOverridden: !!agent.overriddenBy,
|
||||
overriddenBy: agent.overriddenBy || null,
|
||||
}
|
||||
$[8] = sortedAgents;
|
||||
$[9] = source;
|
||||
$[10] = t4;
|
||||
} else {
|
||||
t4 = $[10];
|
||||
}
|
||||
const selectableAgentsInOrder = t4;
|
||||
let t5;
|
||||
let t6;
|
||||
if ($[11] !== isCreateNewSelected || $[12] !== onCreateNew || $[13] !== selectableAgentsInOrder || $[14] !== selectedAgent) {
|
||||
t5 = () => {
|
||||
if (!selectedAgent && !isCreateNewSelected && selectableAgentsInOrder.length > 0) {
|
||||
if (onCreateNew) {
|
||||
setIsCreateNewSelected(true);
|
||||
} else {
|
||||
setSelectedAgent(selectableAgentsInOrder[0] || null);
|
||||
}
|
||||
}
|
||||
};
|
||||
t6 = [selectableAgentsInOrder, selectedAgent, isCreateNewSelected, onCreateNew];
|
||||
$[11] = isCreateNewSelected;
|
||||
$[12] = onCreateNew;
|
||||
$[13] = selectableAgentsInOrder;
|
||||
$[14] = selectedAgent;
|
||||
$[15] = t5;
|
||||
$[16] = t6;
|
||||
} else {
|
||||
t5 = $[15];
|
||||
t6 = $[16];
|
||||
|
||||
const renderCreateNewOption = () => {
|
||||
return (
|
||||
<Box>
|
||||
<Text color={isCreateNewSelected ? 'suggestion' : undefined}>
|
||||
{isCreateNewSelected ? `${figures.pointer} ` : ' '}
|
||||
</Text>
|
||||
<Text color={isCreateNewSelected ? 'suggestion' : undefined}>
|
||||
Create new agent
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
React.useEffect(t5, t6);
|
||||
let t7;
|
||||
if ($[17] !== isCreateNewSelected || $[18] !== onCreateNew || $[19] !== onSelect || $[20] !== selectableAgentsInOrder || $[21] !== selectedAgent) {
|
||||
t7 = e => {
|
||||
if (e.key === "return") {
|
||||
e.preventDefault();
|
||||
if (isCreateNewSelected && onCreateNew) {
|
||||
onCreateNew();
|
||||
} else {
|
||||
if (selectedAgent) {
|
||||
onSelect(selectedAgent);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key !== "up" && e.key !== "down") {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
const hasCreateOption = !!onCreateNew;
|
||||
const totalItems = selectableAgentsInOrder.length + (hasCreateOption ? 1 : 0);
|
||||
if (totalItems === 0) {
|
||||
return;
|
||||
}
|
||||
let currentPosition = 0;
|
||||
if (!isCreateNewSelected && selectedAgent) {
|
||||
const agentIndex = selectableAgentsInOrder.findIndex(a_1 => a_1.agentType === selectedAgent.agentType && a_1.source === selectedAgent.source);
|
||||
if (agentIndex >= 0) {
|
||||
currentPosition = hasCreateOption ? agentIndex + 1 : agentIndex;
|
||||
}
|
||||
}
|
||||
const newPosition = e.key === "up" ? currentPosition === 0 ? totalItems - 1 : currentPosition - 1 : currentPosition === totalItems - 1 ? 0 : currentPosition + 1;
|
||||
if (hasCreateOption && newPosition === 0) {
|
||||
setIsCreateNewSelected(true);
|
||||
setSelectedAgent(null);
|
||||
} else {
|
||||
const agentIndex_0 = hasCreateOption ? newPosition - 1 : newPosition;
|
||||
const newAgent = selectableAgentsInOrder[agentIndex_0];
|
||||
if (newAgent) {
|
||||
setIsCreateNewSelected(false);
|
||||
setSelectedAgent(newAgent);
|
||||
}
|
||||
}
|
||||
};
|
||||
$[17] = isCreateNewSelected;
|
||||
$[18] = onCreateNew;
|
||||
$[19] = onSelect;
|
||||
$[20] = selectableAgentsInOrder;
|
||||
$[21] = selectedAgent;
|
||||
$[22] = t7;
|
||||
} else {
|
||||
t7 = $[22];
|
||||
|
||||
const renderAgent = (agent: ResolvedAgent) => {
|
||||
const isBuiltIn = agent.source === 'built-in'
|
||||
const isSelected =
|
||||
!isBuiltIn &&
|
||||
!isCreateNewSelected &&
|
||||
selectedAgent?.agentType === agent.agentType &&
|
||||
selectedAgent?.source === agent.source
|
||||
|
||||
const { isOverridden, overriddenBy } = getOverrideInfo(agent)
|
||||
const dimmed = isBuiltIn || isOverridden
|
||||
const textColor = !isBuiltIn && isSelected ? 'suggestion' : undefined
|
||||
|
||||
const resolvedModel = resolveAgentModelDisplay(agent)
|
||||
|
||||
return (
|
||||
<Box key={`${agent.agentType}-${agent.source}`}>
|
||||
<Text dimColor={dimmed && !isSelected} color={textColor}>
|
||||
{isBuiltIn ? '' : isSelected ? `${figures.pointer} ` : ' '}
|
||||
</Text>
|
||||
<Text dimColor={dimmed && !isSelected} color={textColor}>
|
||||
{agent.agentType}
|
||||
</Text>
|
||||
{resolvedModel && (
|
||||
<Text dimColor={true} color={textColor}>
|
||||
{' · '}
|
||||
{resolvedModel}
|
||||
</Text>
|
||||
)}
|
||||
{agent.memory && (
|
||||
<Text dimColor={true} color={textColor}>
|
||||
{' · '}
|
||||
{agent.memory} memory
|
||||
</Text>
|
||||
)}
|
||||
{overriddenBy && (
|
||||
<Text
|
||||
dimColor={!isSelected}
|
||||
color={isSelected ? 'warning' : undefined}
|
||||
>
|
||||
{' '}
|
||||
{figures.warning} shadowed by {getOverrideSourceLabel(overriddenBy)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
const handleKeyDown = t7;
|
||||
let t8;
|
||||
if ($[23] !== renderAgent || $[24] !== sortedAgents) {
|
||||
t8 = t9 => {
|
||||
const title = t9 === undefined ? "Built-in (always available):" : t9;
|
||||
const builtInAgents = sortedAgents.filter(_temp4);
|
||||
return <Box flexDirection="column" marginBottom={1} paddingLeft={2}><Text bold={true} dimColor={true}>{title}</Text>{builtInAgents.map(renderAgent)}</Box>;
|
||||
};
|
||||
$[23] = renderAgent;
|
||||
$[24] = sortedAgents;
|
||||
$[25] = t8;
|
||||
} else {
|
||||
t8 = $[25];
|
||||
}
|
||||
const renderBuiltInAgentsSection = t8;
|
||||
let t9;
|
||||
if ($[26] !== renderAgent) {
|
||||
t9 = (title_0, groupAgents) => {
|
||||
if (!groupAgents.length) {
|
||||
return null;
|
||||
}
|
||||
const folderPath = groupAgents[0]?.baseDir;
|
||||
return <Box flexDirection="column" marginBottom={1}><Box paddingLeft={2}><Text bold={true} dimColor={true}>{title_0}</Text>{folderPath && <Text dimColor={true}> ({folderPath})</Text>}</Box>{groupAgents.map(agent_1 => renderAgent(agent_1))}</Box>;
|
||||
};
|
||||
$[26] = renderAgent;
|
||||
$[27] = t9;
|
||||
} else {
|
||||
t9 = $[27];
|
||||
}
|
||||
const renderAgentGroup = t9;
|
||||
let t10;
|
||||
if ($[28] !== source) {
|
||||
t10 = getAgentSourceDisplayName(source);
|
||||
$[28] = source;
|
||||
$[29] = t10;
|
||||
} else {
|
||||
t10 = $[29];
|
||||
}
|
||||
const sourceTitle = t10;
|
||||
let T0;
|
||||
let T1;
|
||||
let t11;
|
||||
let t12;
|
||||
let t13;
|
||||
let t14;
|
||||
let t15;
|
||||
let t16;
|
||||
let t17;
|
||||
let t18;
|
||||
let t19;
|
||||
let t20;
|
||||
let t21;
|
||||
let t22;
|
||||
if ($[30] !== changes || $[31] !== handleKeyDown || $[32] !== onBack || $[33] !== onCreateNew || $[34] !== renderAgent || $[35] !== renderAgentGroup || $[36] !== renderBuiltInAgentsSection || $[37] !== renderCreateNewOption || $[38] !== sortedAgents || $[39] !== source || $[40] !== sourceTitle) {
|
||||
t22 = Symbol.for("react.early_return_sentinel");
|
||||
bb1: {
|
||||
const builtInAgents_0 = sortedAgents.filter(_temp5);
|
||||
const hasNoAgents = !sortedAgents.length || source !== "built-in" && !sortedAgents.some(_temp6);
|
||||
if (hasNoAgents) {
|
||||
let t23;
|
||||
if ($[55] !== onCreateNew || $[56] !== renderCreateNewOption) {
|
||||
t23 = onCreateNew && <Box>{renderCreateNewOption()}</Box>;
|
||||
$[55] = onCreateNew;
|
||||
$[56] = renderCreateNewOption;
|
||||
$[57] = t23;
|
||||
} else {
|
||||
t23 = $[57];
|
||||
}
|
||||
let t24;
|
||||
let t25;
|
||||
let t26;
|
||||
if ($[58] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t24 = <Text dimColor={true}>No agents found. Create specialized subagents that Claude can delegate to.</Text>;
|
||||
t25 = <Text dimColor={true}>Each subagent has its own context window, custom system prompt, and specific tools.</Text>;
|
||||
t26 = <Text dimColor={true}>Try creating: Code Reviewer, Code Simplifier, Security Reviewer, Tech Lead, or UX Reviewer.</Text>;
|
||||
$[58] = t24;
|
||||
$[59] = t25;
|
||||
$[60] = t26;
|
||||
} else {
|
||||
t24 = $[58];
|
||||
t25 = $[59];
|
||||
t26 = $[60];
|
||||
}
|
||||
let t27;
|
||||
if ($[61] !== renderBuiltInAgentsSection || $[62] !== sortedAgents || $[63] !== source) {
|
||||
t27 = source !== "built-in" && sortedAgents.some(_temp7) && <><Divider />{renderBuiltInAgentsSection()}</>;
|
||||
$[61] = renderBuiltInAgentsSection;
|
||||
$[62] = sortedAgents;
|
||||
$[63] = source;
|
||||
$[64] = t27;
|
||||
} else {
|
||||
t27 = $[64];
|
||||
}
|
||||
let t28;
|
||||
if ($[65] !== handleKeyDown || $[66] !== t23 || $[67] !== t27) {
|
||||
t28 = <Box flexDirection="column" gap={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t23}{t24}{t25}{t26}{t27}</Box>;
|
||||
$[65] = handleKeyDown;
|
||||
$[66] = t23;
|
||||
$[67] = t27;
|
||||
$[68] = t28;
|
||||
} else {
|
||||
t28 = $[68];
|
||||
}
|
||||
let t29;
|
||||
if ($[69] !== onBack || $[70] !== sourceTitle || $[71] !== t28) {
|
||||
t29 = <Dialog title={sourceTitle} subtitle="No agents found" onCancel={onBack} hideInputGuide={true}>{t28}</Dialog>;
|
||||
$[69] = onBack;
|
||||
$[70] = sourceTitle;
|
||||
$[71] = t28;
|
||||
$[72] = t29;
|
||||
} else {
|
||||
t29 = $[72];
|
||||
}
|
||||
t22 = t29;
|
||||
break bb1;
|
||||
}
|
||||
T1 = Dialog;
|
||||
t17 = sourceTitle;
|
||||
let t23;
|
||||
if ($[73] !== sortedAgents) {
|
||||
t23 = count(sortedAgents, _temp8);
|
||||
$[73] = sortedAgents;
|
||||
$[74] = t23;
|
||||
} else {
|
||||
t23 = $[74];
|
||||
}
|
||||
t18 = `${t23} agents`;
|
||||
t19 = onBack;
|
||||
t20 = true;
|
||||
if ($[75] !== changes) {
|
||||
t21 = changes && changes.length > 0 && <Box marginTop={1}><Text dimColor={true}>{changes[changes.length - 1]}</Text></Box>;
|
||||
$[75] = changes;
|
||||
$[76] = t21;
|
||||
} else {
|
||||
t21 = $[76];
|
||||
}
|
||||
T0 = Box;
|
||||
t11 = "column";
|
||||
t12 = 0;
|
||||
t13 = true;
|
||||
t14 = handleKeyDown;
|
||||
if ($[77] !== onCreateNew || $[78] !== renderCreateNewOption) {
|
||||
t15 = onCreateNew && <Box marginBottom={1}>{renderCreateNewOption()}</Box>;
|
||||
$[77] = onCreateNew;
|
||||
$[78] = renderCreateNewOption;
|
||||
$[79] = t15;
|
||||
} else {
|
||||
t15 = $[79];
|
||||
}
|
||||
t16 = source === "all" ? <>{AGENT_SOURCE_GROUPS.filter(_temp9).map(t24 => {
|
||||
const {
|
||||
label,
|
||||
source: groupSource_0
|
||||
} = t24;
|
||||
return <React.Fragment key={groupSource_0}>{renderAgentGroup(label, sortedAgents.filter(a_7 => a_7.source === groupSource_0))}</React.Fragment>;
|
||||
})}{builtInAgents_0.length > 0 && <Box flexDirection="column" marginBottom={1} paddingLeft={2}><Text dimColor={true}><Text bold={true}>Built-in agents</Text> (always available)</Text>{builtInAgents_0.map(renderAgent)}</Box>}</> : source === "built-in" ? <><Text dimColor={true} italic={true}>Built-in agents are provided by default and cannot be modified.</Text><Box marginTop={1} flexDirection="column">{sortedAgents.map(agent_2 => renderAgent(agent_2))}</Box></> : <>{sortedAgents.filter(_temp0).map(agent_3 => renderAgent(agent_3))}{sortedAgents.some(_temp1) && <><Divider />{renderBuiltInAgentsSection()}</>}</>;
|
||||
|
||||
const selectableAgentsInOrder = React.useMemo(() => {
|
||||
const nonBuiltIn = sortedAgents.filter(a => a.source !== 'built-in')
|
||||
if (source === 'all') {
|
||||
return AGENT_SOURCE_GROUPS.filter(g => g.source !== 'built-in').flatMap(
|
||||
({ source: groupSource }) =>
|
||||
nonBuiltIn.filter(a => a.source === groupSource),
|
||||
)
|
||||
}
|
||||
return nonBuiltIn
|
||||
}, [sortedAgents, source])
|
||||
|
||||
// Set initial selection
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
!selectedAgent &&
|
||||
!isCreateNewSelected &&
|
||||
selectableAgentsInOrder.length > 0
|
||||
) {
|
||||
if (onCreateNew) {
|
||||
setIsCreateNewSelected(true)
|
||||
} else {
|
||||
setSelectedAgent(selectableAgentsInOrder[0] || null)
|
||||
}
|
||||
}
|
||||
}, [selectableAgentsInOrder, selectedAgent, isCreateNewSelected, onCreateNew])
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'return') {
|
||||
e.preventDefault()
|
||||
if (isCreateNewSelected && onCreateNew) {
|
||||
onCreateNew()
|
||||
} else if (selectedAgent) {
|
||||
onSelect(selectedAgent)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key !== 'up' && e.key !== 'down') return
|
||||
e.preventDefault()
|
||||
|
||||
// Handle navigation with "Create New Agent" option
|
||||
const hasCreateOption = !!onCreateNew
|
||||
const totalItems =
|
||||
selectableAgentsInOrder.length + (hasCreateOption ? 1 : 0)
|
||||
|
||||
if (totalItems === 0) return
|
||||
|
||||
// Calculate current position in list (0 = create new, 1+ = agents)
|
||||
let currentPosition = 0
|
||||
if (!isCreateNewSelected && selectedAgent) {
|
||||
const agentIndex = selectableAgentsInOrder.findIndex(
|
||||
a =>
|
||||
a.agentType === selectedAgent.agentType &&
|
||||
a.source === selectedAgent.source,
|
||||
)
|
||||
if (agentIndex >= 0) {
|
||||
currentPosition = hasCreateOption ? agentIndex + 1 : agentIndex
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate new position with wrap-around
|
||||
const newPosition =
|
||||
e.key === 'up'
|
||||
? currentPosition === 0
|
||||
? totalItems - 1
|
||||
: currentPosition - 1
|
||||
: currentPosition === totalItems - 1
|
||||
? 0
|
||||
: currentPosition + 1
|
||||
|
||||
// Update selection based on new position
|
||||
if (hasCreateOption && newPosition === 0) {
|
||||
setIsCreateNewSelected(true)
|
||||
setSelectedAgent(null)
|
||||
} else {
|
||||
const agentIndex = hasCreateOption ? newPosition - 1 : newPosition
|
||||
const newAgent = selectableAgentsInOrder[agentIndex]
|
||||
if (newAgent) {
|
||||
setIsCreateNewSelected(false)
|
||||
setSelectedAgent(newAgent)
|
||||
}
|
||||
}
|
||||
$[30] = changes;
|
||||
$[31] = handleKeyDown;
|
||||
$[32] = onBack;
|
||||
$[33] = onCreateNew;
|
||||
$[34] = renderAgent;
|
||||
$[35] = renderAgentGroup;
|
||||
$[36] = renderBuiltInAgentsSection;
|
||||
$[37] = renderCreateNewOption;
|
||||
$[38] = sortedAgents;
|
||||
$[39] = source;
|
||||
$[40] = sourceTitle;
|
||||
$[41] = T0;
|
||||
$[42] = T1;
|
||||
$[43] = t11;
|
||||
$[44] = t12;
|
||||
$[45] = t13;
|
||||
$[46] = t14;
|
||||
$[47] = t15;
|
||||
$[48] = t16;
|
||||
$[49] = t17;
|
||||
$[50] = t18;
|
||||
$[51] = t19;
|
||||
$[52] = t20;
|
||||
$[53] = t21;
|
||||
$[54] = t22;
|
||||
} else {
|
||||
T0 = $[41];
|
||||
T1 = $[42];
|
||||
t11 = $[43];
|
||||
t12 = $[44];
|
||||
t13 = $[45];
|
||||
t14 = $[46];
|
||||
t15 = $[47];
|
||||
t16 = $[48];
|
||||
t17 = $[49];
|
||||
t18 = $[50];
|
||||
t19 = $[51];
|
||||
t20 = $[52];
|
||||
t21 = $[53];
|
||||
t22 = $[54];
|
||||
}
|
||||
if (t22 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t22;
|
||||
|
||||
const renderBuiltInAgentsSection = (
|
||||
title = 'Built-in (always available):',
|
||||
) => {
|
||||
const builtInAgents = sortedAgents.filter(a => a.source === 'built-in')
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} paddingLeft={2}>
|
||||
<Text bold dimColor>
|
||||
{title}
|
||||
</Text>
|
||||
{builtInAgents.map(renderAgent)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
let t23;
|
||||
if ($[80] !== T0 || $[81] !== t11 || $[82] !== t12 || $[83] !== t13 || $[84] !== t14 || $[85] !== t15 || $[86] !== t16) {
|
||||
t23 = <T0 flexDirection={t11} tabIndex={t12} autoFocus={t13} onKeyDown={t14}>{t15}{t16}</T0>;
|
||||
$[80] = T0;
|
||||
$[81] = t11;
|
||||
$[82] = t12;
|
||||
$[83] = t13;
|
||||
$[84] = t14;
|
||||
$[85] = t15;
|
||||
$[86] = t16;
|
||||
$[87] = t23;
|
||||
} else {
|
||||
t23 = $[87];
|
||||
|
||||
const renderAgentGroup = (title: string, groupAgents: ResolvedAgent[]) => {
|
||||
if (!groupAgents.length) return null
|
||||
|
||||
const folderPath = groupAgents[0]?.baseDir
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box paddingLeft={2}>
|
||||
<Text bold dimColor>
|
||||
{title}
|
||||
</Text>
|
||||
{folderPath && <Text dimColor> ({folderPath})</Text>}
|
||||
</Box>
|
||||
{groupAgents.map(agent => renderAgent(agent))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
let t24;
|
||||
if ($[88] !== T1 || $[89] !== t17 || $[90] !== t18 || $[91] !== t19 || $[92] !== t20 || $[93] !== t21 || $[94] !== t23) {
|
||||
t24 = <T1 title={t17} subtitle={t18} onCancel={t19} hideInputGuide={t20}>{t21}{t23}</T1>;
|
||||
$[88] = T1;
|
||||
$[89] = t17;
|
||||
$[90] = t18;
|
||||
$[91] = t19;
|
||||
$[92] = t20;
|
||||
$[93] = t21;
|
||||
$[94] = t23;
|
||||
$[95] = t24;
|
||||
} else {
|
||||
t24 = $[95];
|
||||
|
||||
const sourceTitle = getAgentSourceDisplayName(source)
|
||||
|
||||
const builtInAgents = sortedAgents.filter(a => a.source === 'built-in')
|
||||
|
||||
const hasNoAgents =
|
||||
!sortedAgents.length ||
|
||||
(source !== 'built-in' && !sortedAgents.some(a => a.source !== 'built-in'))
|
||||
|
||||
if (hasNoAgents) {
|
||||
return (
|
||||
<Dialog
|
||||
title={sourceTitle}
|
||||
subtitle="No agents found"
|
||||
onCancel={onBack}
|
||||
hideInputGuide
|
||||
>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{onCreateNew && <Box>{renderCreateNewOption()}</Box>}
|
||||
<Text dimColor>
|
||||
No agents found. Create specialized subagents that Claude can
|
||||
delegate to.
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
Each subagent has its own context window, custom system prompt, and
|
||||
specific tools.
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
Try creating: Code Reviewer, Code Simplifier, Security Reviewer,
|
||||
Tech Lead, or UX Reviewer.
|
||||
</Text>
|
||||
{source !== 'built-in' &&
|
||||
sortedAgents.some(a => a.source === 'built-in') && (
|
||||
<>
|
||||
<Divider />
|
||||
{renderBuiltInAgentsSection()}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
return t24;
|
||||
}
|
||||
function _temp1(a_9) {
|
||||
return a_9.source === "built-in";
|
||||
}
|
||||
function _temp0(a_8) {
|
||||
return a_8.source !== "built-in";
|
||||
}
|
||||
function _temp9(g_0) {
|
||||
return g_0.source !== "built-in";
|
||||
}
|
||||
function _temp8(a_6) {
|
||||
return !a_6.overriddenBy;
|
||||
}
|
||||
function _temp7(a_5) {
|
||||
return a_5.source === "built-in";
|
||||
}
|
||||
function _temp6(a_4) {
|
||||
return a_4.source !== "built-in";
|
||||
}
|
||||
function _temp5(a_3) {
|
||||
return a_3.source === "built-in";
|
||||
}
|
||||
function _temp4(a_2) {
|
||||
return a_2.source === "built-in";
|
||||
}
|
||||
function _temp3(g) {
|
||||
return g.source !== "built-in";
|
||||
}
|
||||
function _temp2(a) {
|
||||
return a.source !== "built-in";
|
||||
}
|
||||
function _temp(agent) {
|
||||
return {
|
||||
isOverridden: !!agent.overriddenBy,
|
||||
overriddenBy: agent.overriddenBy || null
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={sourceTitle}
|
||||
subtitle={`${count(sortedAgents, a => !a.overriddenBy)} agents`}
|
||||
onCancel={onBack}
|
||||
hideInputGuide
|
||||
>
|
||||
{changes && changes.length > 0 && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>{changes[changes.length - 1]}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
flexDirection="column"
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{onCreateNew && <Box marginBottom={1}>{renderCreateNewOption()}</Box>}
|
||||
{source === 'all' ? (
|
||||
<>
|
||||
{AGENT_SOURCE_GROUPS.filter(g => g.source !== 'built-in').map(
|
||||
({ label, source: groupSource }) => (
|
||||
<React.Fragment key={groupSource}>
|
||||
{renderAgentGroup(
|
||||
label,
|
||||
sortedAgents.filter(a => a.source === groupSource),
|
||||
)}
|
||||
</React.Fragment>
|
||||
),
|
||||
)}
|
||||
{builtInAgents.length > 0 && (
|
||||
<Box flexDirection="column" marginBottom={1} paddingLeft={2}>
|
||||
<Text dimColor>
|
||||
<Text bold>Built-in agents</Text> (always available)
|
||||
</Text>
|
||||
{builtInAgents.map(renderAgent)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : source === 'built-in' ? (
|
||||
<>
|
||||
<Text dimColor italic>
|
||||
Built-in agents are provided by default and cannot be modified.
|
||||
</Text>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{sortedAgents.map(agent => renderAgent(agent))}
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{sortedAgents
|
||||
.filter(a => a.source !== 'built-in')
|
||||
.map(agent => renderAgent(agent))}
|
||||
{sortedAgents.some(a => a.source === 'built-in') && (
|
||||
<>
|
||||
<Divider />
|
||||
{renderBuiltInAgentsSection()}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,111 +1,106 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React, { useState } from 'react';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from '../../tools/AgentTool/agentColorManager.js';
|
||||
import { capitalize } from '../../utils/stringUtils.js';
|
||||
type ColorOption = AgentColorName | 'automatic';
|
||||
const COLOR_OPTIONS: ColorOption[] = ['automatic', ...AGENT_COLORS];
|
||||
import figures from 'figures'
|
||||
import React, { useState } from 'react'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import {
|
||||
AGENT_COLOR_TO_THEME_COLOR,
|
||||
AGENT_COLORS,
|
||||
type AgentColorName,
|
||||
} from '../../tools/AgentTool/agentColorManager.js'
|
||||
import { capitalize } from '../../utils/stringUtils.js'
|
||||
|
||||
type ColorOption = AgentColorName | 'automatic'
|
||||
|
||||
const COLOR_OPTIONS: ColorOption[] = ['automatic', ...AGENT_COLORS]
|
||||
|
||||
type Props = {
|
||||
agentName: string;
|
||||
currentColor?: AgentColorName | 'automatic';
|
||||
onConfirm: (color: AgentColorName | undefined) => void;
|
||||
};
|
||||
export function ColorPicker(t0) {
|
||||
const $ = _c(17);
|
||||
const {
|
||||
agentName,
|
||||
currentColor: t1,
|
||||
onConfirm
|
||||
} = t0;
|
||||
const currentColor = t1 === undefined ? "automatic" : t1;
|
||||
let t2;
|
||||
if ($[0] !== currentColor) {
|
||||
t2 = COLOR_OPTIONS.findIndex(opt => opt === currentColor);
|
||||
$[0] = currentColor;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const [selectedIndex, setSelectedIndex] = useState(Math.max(0, t2));
|
||||
let t3;
|
||||
if ($[2] !== onConfirm || $[3] !== selectedIndex) {
|
||||
t3 = e => {
|
||||
if (e.key === "up") {
|
||||
e.preventDefault();
|
||||
setSelectedIndex(_temp);
|
||||
} else {
|
||||
if (e.key === "down") {
|
||||
e.preventDefault();
|
||||
setSelectedIndex(_temp2);
|
||||
} else {
|
||||
if (e.key === "return") {
|
||||
e.preventDefault();
|
||||
const selected = COLOR_OPTIONS[selectedIndex];
|
||||
onConfirm(selected === "automatic" ? undefined : selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
$[2] = onConfirm;
|
||||
$[3] = selectedIndex;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
const handleKeyDown = t3;
|
||||
const selectedValue = COLOR_OPTIONS[selectedIndex];
|
||||
let t4;
|
||||
if ($[5] !== selectedIndex) {
|
||||
t4 = COLOR_OPTIONS.map((option, index) => {
|
||||
const isSelected = index === selectedIndex;
|
||||
return <Box key={option} flexDirection="row" gap={1}><Text color={isSelected ? "suggestion" : undefined}>{isSelected ? figures.pointer : " "}</Text>{option === "automatic" ? <Text bold={isSelected}>Automatic color</Text> : <Box gap={1}><Text backgroundColor={AGENT_COLOR_TO_THEME_COLOR[option]} color="inverseText">{" "}</Text><Text bold={isSelected}>{capitalize(option)}</Text></Box>}</Box>;
|
||||
});
|
||||
$[5] = selectedIndex;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== t4) {
|
||||
t5 = <Box flexDirection="column">{t4}</Box>;
|
||||
$[7] = t4;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
let t6;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Text>Preview: </Text>;
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
let t7;
|
||||
if ($[10] !== agentName || $[11] !== selectedValue) {
|
||||
t7 = <Box marginTop={1}>{t6}{selectedValue === undefined || selectedValue === "automatic" ? <Text inverse={true} bold={true}>{" "}@{agentName}{" "}</Text> : <Text backgroundColor={AGENT_COLOR_TO_THEME_COLOR[selectedValue]} color="inverseText" bold={true}>{" "}@{agentName}{" "}</Text>}</Box>;
|
||||
$[10] = agentName;
|
||||
$[11] = selectedValue;
|
||||
$[12] = t7;
|
||||
} else {
|
||||
t7 = $[12];
|
||||
}
|
||||
let t8;
|
||||
if ($[13] !== handleKeyDown || $[14] !== t5 || $[15] !== t7) {
|
||||
t8 = <Box flexDirection="column" gap={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t5}{t7}</Box>;
|
||||
$[13] = handleKeyDown;
|
||||
$[14] = t5;
|
||||
$[15] = t7;
|
||||
$[16] = t8;
|
||||
} else {
|
||||
t8 = $[16];
|
||||
}
|
||||
return t8;
|
||||
agentName: string
|
||||
currentColor?: AgentColorName | 'automatic'
|
||||
onConfirm: (color: AgentColorName | undefined) => void
|
||||
}
|
||||
function _temp2(prev_0) {
|
||||
return prev_0 < COLOR_OPTIONS.length - 1 ? prev_0 + 1 : 0;
|
||||
}
|
||||
function _temp(prev) {
|
||||
return prev > 0 ? prev - 1 : COLOR_OPTIONS.length - 1;
|
||||
|
||||
export function ColorPicker({
|
||||
agentName,
|
||||
currentColor = 'automatic',
|
||||
onConfirm,
|
||||
}: Props): React.ReactNode {
|
||||
const [selectedIndex, setSelectedIndex] = useState(
|
||||
Math.max(
|
||||
0,
|
||||
COLOR_OPTIONS.findIndex(opt => opt === currentColor),
|
||||
),
|
||||
)
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'up') {
|
||||
e.preventDefault()
|
||||
setSelectedIndex(prev => (prev > 0 ? prev - 1 : COLOR_OPTIONS.length - 1))
|
||||
} else if (e.key === 'down') {
|
||||
e.preventDefault()
|
||||
setSelectedIndex(prev => (prev < COLOR_OPTIONS.length - 1 ? prev + 1 : 0))
|
||||
} else if (e.key === 'return') {
|
||||
e.preventDefault()
|
||||
const selected = COLOR_OPTIONS[selectedIndex]
|
||||
onConfirm(selected === 'automatic' ? undefined : selected)
|
||||
}
|
||||
}
|
||||
|
||||
const selectedValue = COLOR_OPTIONS[selectedIndex]
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
{COLOR_OPTIONS.map((option, index) => {
|
||||
const isSelected = index === selectedIndex
|
||||
|
||||
return (
|
||||
<Box key={option} flexDirection="row" gap={1}>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? figures.pointer : ' '}
|
||||
</Text>
|
||||
|
||||
{option === 'automatic' ? (
|
||||
<Text bold={isSelected}>Automatic color</Text>
|
||||
) : (
|
||||
<Box gap={1}>
|
||||
<Text
|
||||
backgroundColor={AGENT_COLOR_TO_THEME_COLOR[option]}
|
||||
color="inverseText"
|
||||
>
|
||||
{' '}
|
||||
</Text>
|
||||
<Text bold={isSelected}>{capitalize(option)}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text>Preview: </Text>
|
||||
{selectedValue === undefined || selectedValue === 'automatic' ? (
|
||||
<Text inverse bold>
|
||||
{' '}
|
||||
@{agentName}{' '}
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
backgroundColor={AGENT_COLOR_TO_THEME_COLOR[selectedValue]}
|
||||
color="inverseText"
|
||||
bold
|
||||
>
|
||||
{' '}
|
||||
@{agentName}{' '}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,67 +1,52 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { getAgentModelOptions } from '../../utils/model/agent.js';
|
||||
import { Select } from '../CustomSelect/select.js';
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { getAgentModelOptions } from '../../utils/model/agent.js'
|
||||
import { Select } from '../CustomSelect/select.js'
|
||||
|
||||
interface ModelSelectorProps {
|
||||
initialModel?: string;
|
||||
onComplete: (model?: string) => void;
|
||||
onCancel?: () => void;
|
||||
initialModel?: string
|
||||
onComplete: (model?: string) => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
export function ModelSelector(t0) {
|
||||
const $ = _c(11);
|
||||
const {
|
||||
initialModel,
|
||||
onComplete,
|
||||
onCancel
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== initialModel) {
|
||||
bb0: {
|
||||
const base = getAgentModelOptions();
|
||||
if (initialModel && !base.some(o => o.value === initialModel)) {
|
||||
t1 = [{
|
||||
|
||||
export function ModelSelector({
|
||||
initialModel,
|
||||
onComplete,
|
||||
onCancel,
|
||||
}: ModelSelectorProps): React.ReactNode {
|
||||
const modelOptions = React.useMemo(() => {
|
||||
const base = getAgentModelOptions()
|
||||
// If the agent's current model is a full ID (e.g. 'claude-opus-4-5') not
|
||||
// in the alias list, inject it as an option so it can round-trip through
|
||||
// confirm without being overwritten.
|
||||
if (initialModel && !base.some(o => o.value === initialModel)) {
|
||||
return [
|
||||
{
|
||||
value: initialModel,
|
||||
label: initialModel,
|
||||
description: "Current model (custom ID)"
|
||||
}, ...base];
|
||||
break bb0;
|
||||
}
|
||||
t1 = base;
|
||||
description: 'Current model (custom ID)',
|
||||
},
|
||||
...base,
|
||||
]
|
||||
}
|
||||
$[0] = initialModel;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const modelOptions = t1;
|
||||
const defaultModel = initialModel ?? "sonnet";
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box marginBottom={1}><Text dimColor={true}>Model determines the agent's reasoning capabilities and speed.</Text></Box>;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== onCancel || $[4] !== onComplete) {
|
||||
t3 = () => onCancel ? onCancel() : onComplete(undefined);
|
||||
$[3] = onCancel;
|
||||
$[4] = onComplete;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== defaultModel || $[7] !== modelOptions || $[8] !== onComplete || $[9] !== t3) {
|
||||
t4 = <Box flexDirection="column">{t2}<Select options={modelOptions} defaultValue={defaultModel} onChange={onComplete} onCancel={t3} /></Box>;
|
||||
$[6] = defaultModel;
|
||||
$[7] = modelOptions;
|
||||
$[8] = onComplete;
|
||||
$[9] = t3;
|
||||
$[10] = t4;
|
||||
} else {
|
||||
t4 = $[10];
|
||||
}
|
||||
return t4;
|
||||
return base
|
||||
}, [initialModel])
|
||||
|
||||
const defaultModel = initialModel ?? 'sonnet'
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text dimColor>
|
||||
Model determines the agent's reasoning capabilities and speed.
|
||||
</Text>
|
||||
</Box>
|
||||
<Select
|
||||
options={modelOptions}
|
||||
defaultValue={defaultModel}
|
||||
onChange={onComplete}
|
||||
onCancel={() => (onCancel ? onCancel() : onComplete(undefined))}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,561 +1,478 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { mcpInfoFromString } from 'src/services/mcp/mcpStringUtils.js';
|
||||
import { isMcpTool } from 'src/services/mcp/utils.js';
|
||||
import type { Tool, Tools } from 'src/Tool.js';
|
||||
import { filterToolsForAgent } from 'src/tools/AgentTool/agentToolUtils.js';
|
||||
import { AGENT_TOOL_NAME } from 'src/tools/AgentTool/constants.js';
|
||||
import { BashTool } from 'src/tools/BashTool/BashTool.js';
|
||||
import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js';
|
||||
import { FileEditTool } from 'src/tools/FileEditTool/FileEditTool.js';
|
||||
import { FileReadTool } from 'src/tools/FileReadTool/FileReadTool.js';
|
||||
import { FileWriteTool } from 'src/tools/FileWriteTool/FileWriteTool.js';
|
||||
import { GlobTool } from 'src/tools/GlobTool/GlobTool.js';
|
||||
import { GrepTool } from 'src/tools/GrepTool/GrepTool.js';
|
||||
import { ListMcpResourcesTool } from 'src/tools/ListMcpResourcesTool/ListMcpResourcesTool.js';
|
||||
import { NotebookEditTool } from 'src/tools/NotebookEditTool/NotebookEditTool.js';
|
||||
import { ReadMcpResourceTool } from 'src/tools/ReadMcpResourceTool/ReadMcpResourceTool.js';
|
||||
import { TaskOutputTool } from 'src/tools/TaskOutputTool/TaskOutputTool.js';
|
||||
import { TaskStopTool } from 'src/tools/TaskStopTool/TaskStopTool.js';
|
||||
import { TodoWriteTool } from 'src/tools/TodoWriteTool/TodoWriteTool.js';
|
||||
import { TungstenTool } from 'src/tools/TungstenTool/TungstenTool.js';
|
||||
import { WebFetchTool } from 'src/tools/WebFetchTool/WebFetchTool.js';
|
||||
import { WebSearchTool } from 'src/tools/WebSearchTool/WebSearchTool.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import { count } from '../../utils/array.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import { Divider } from '../design-system/Divider.js';
|
||||
import figures from 'figures'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { mcpInfoFromString } from 'src/services/mcp/mcpStringUtils.js'
|
||||
import { isMcpTool } from 'src/services/mcp/utils.js'
|
||||
import type { Tool, Tools } from 'src/Tool.js'
|
||||
import { filterToolsForAgent } from 'src/tools/AgentTool/agentToolUtils.js'
|
||||
import { AGENT_TOOL_NAME } from 'src/tools/AgentTool/constants.js'
|
||||
import { BashTool } from 'src/tools/BashTool/BashTool.js'
|
||||
import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
|
||||
import { FileEditTool } from 'src/tools/FileEditTool/FileEditTool.js'
|
||||
import { FileReadTool } from 'src/tools/FileReadTool/FileReadTool.js'
|
||||
import { FileWriteTool } from 'src/tools/FileWriteTool/FileWriteTool.js'
|
||||
import { GlobTool } from 'src/tools/GlobTool/GlobTool.js'
|
||||
import { GrepTool } from 'src/tools/GrepTool/GrepTool.js'
|
||||
import { ListMcpResourcesTool } from 'src/tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
|
||||
import { NotebookEditTool } from 'src/tools/NotebookEditTool/NotebookEditTool.js'
|
||||
import { ReadMcpResourceTool } from 'src/tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
|
||||
import { TaskOutputTool } from 'src/tools/TaskOutputTool/TaskOutputTool.js'
|
||||
import { TaskStopTool } from 'src/tools/TaskStopTool/TaskStopTool.js'
|
||||
import { TodoWriteTool } from 'src/tools/TodoWriteTool/TodoWriteTool.js'
|
||||
import { TungstenTool } from 'src/tools/TungstenTool/TungstenTool.js'
|
||||
import { WebFetchTool } from 'src/tools/WebFetchTool/WebFetchTool.js'
|
||||
import { WebSearchTool } from 'src/tools/WebSearchTool/WebSearchTool.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import { count } from '../../utils/array.js'
|
||||
import { plural } from '../../utils/stringUtils.js'
|
||||
import { Divider } from '../design-system/Divider.js'
|
||||
|
||||
type Props = {
|
||||
tools: Tools;
|
||||
initialTools: string[] | undefined;
|
||||
onComplete: (selectedTools: string[] | undefined) => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
tools: Tools
|
||||
initialTools: string[] | undefined
|
||||
onComplete: (selectedTools: string[] | undefined) => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
type ToolBucket = {
|
||||
name: string;
|
||||
toolNames: Set<string>;
|
||||
isMcp?: boolean;
|
||||
};
|
||||
name: string
|
||||
toolNames: Set<string>
|
||||
isMcp?: boolean
|
||||
}
|
||||
|
||||
type ToolBuckets = {
|
||||
READ_ONLY: ToolBucket;
|
||||
EDIT: ToolBucket;
|
||||
EXECUTION: ToolBucket;
|
||||
MCP: ToolBucket;
|
||||
OTHER: ToolBucket;
|
||||
};
|
||||
READ_ONLY: ToolBucket
|
||||
EDIT: ToolBucket
|
||||
EXECUTION: ToolBucket
|
||||
MCP: ToolBucket
|
||||
OTHER: ToolBucket
|
||||
}
|
||||
|
||||
function getToolBuckets(): ToolBuckets {
|
||||
return {
|
||||
READ_ONLY: {
|
||||
name: 'Read-only tools',
|
||||
toolNames: new Set([GlobTool.name, GrepTool.name, ExitPlanModeV2Tool.name, FileReadTool.name, WebFetchTool.name, TodoWriteTool.name, WebSearchTool.name, TaskStopTool.name, TaskOutputTool.name, ListMcpResourcesTool.name, ReadMcpResourceTool.name])
|
||||
toolNames: new Set([
|
||||
GlobTool.name,
|
||||
GrepTool.name,
|
||||
ExitPlanModeV2Tool.name,
|
||||
FileReadTool.name,
|
||||
WebFetchTool.name,
|
||||
TodoWriteTool.name,
|
||||
WebSearchTool.name,
|
||||
TaskStopTool.name,
|
||||
TaskOutputTool.name,
|
||||
ListMcpResourcesTool.name,
|
||||
ReadMcpResourceTool.name,
|
||||
]),
|
||||
},
|
||||
EDIT: {
|
||||
name: 'Edit tools',
|
||||
toolNames: new Set([FileEditTool.name, FileWriteTool.name, NotebookEditTool.name])
|
||||
toolNames: new Set([
|
||||
FileEditTool.name,
|
||||
FileWriteTool.name,
|
||||
NotebookEditTool.name,
|
||||
]),
|
||||
},
|
||||
EXECUTION: {
|
||||
name: 'Execution tools',
|
||||
toolNames: new Set([BashTool.name, (process.env.USER_TYPE) === 'ant' ? TungstenTool.name : undefined].filter(n => n !== undefined))
|
||||
toolNames: new Set(
|
||||
[
|
||||
BashTool.name,
|
||||
process.env.USER_TYPE === 'ant' ? TungstenTool.name : undefined,
|
||||
].filter(n => n !== undefined),
|
||||
),
|
||||
},
|
||||
MCP: {
|
||||
name: 'MCP tools',
|
||||
toolNames: new Set(),
|
||||
// Dynamic - no static list
|
||||
isMcp: true
|
||||
toolNames: new Set(), // Dynamic - no static list
|
||||
isMcp: true,
|
||||
},
|
||||
OTHER: {
|
||||
name: 'Other tools',
|
||||
toolNames: new Set() // Dynamic - catch-all for uncategorized tools
|
||||
}
|
||||
};
|
||||
toolNames: new Set(), // Dynamic - catch-all for uncategorized tools
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get MCP server buckets dynamically
|
||||
function getMcpServerBuckets(tools: Tools): Array<{
|
||||
serverName: string;
|
||||
tools: Tools;
|
||||
serverName: string
|
||||
tools: Tools
|
||||
}> {
|
||||
const serverMap = new Map<string, Tool[]>();
|
||||
const serverMap = new Map<string, Tool[]>()
|
||||
|
||||
tools.forEach(tool => {
|
||||
if (isMcpTool(tool)) {
|
||||
const mcpInfo = mcpInfoFromString(tool.name);
|
||||
const mcpInfo = mcpInfoFromString(tool.name)
|
||||
if (mcpInfo?.serverName) {
|
||||
const existing = serverMap.get(mcpInfo.serverName) || [];
|
||||
existing.push(tool);
|
||||
serverMap.set(mcpInfo.serverName, existing);
|
||||
const existing = serverMap.get(mcpInfo.serverName) || []
|
||||
existing.push(tool)
|
||||
serverMap.set(mcpInfo.serverName, existing)
|
||||
}
|
||||
}
|
||||
});
|
||||
return Array.from(serverMap.entries()).map(([serverName, tools]) => ({
|
||||
serverName,
|
||||
tools
|
||||
})).sort((a, b) => a.serverName.localeCompare(b.serverName));
|
||||
})
|
||||
|
||||
return Array.from(serverMap.entries())
|
||||
.map(([serverName, tools]) => ({ serverName, tools }))
|
||||
.sort((a, b) => a.serverName.localeCompare(b.serverName))
|
||||
}
|
||||
export function ToolSelector(t0) {
|
||||
const $ = _c(69);
|
||||
const {
|
||||
tools,
|
||||
initialTools,
|
||||
onComplete,
|
||||
onCancel
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== tools) {
|
||||
t1 = filterToolsForAgent({
|
||||
tools,
|
||||
isBuiltIn: false,
|
||||
isAsync: false
|
||||
});
|
||||
$[0] = tools;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
|
||||
export function ToolSelector({
|
||||
tools,
|
||||
initialTools,
|
||||
onComplete,
|
||||
onCancel,
|
||||
}: Props): React.ReactNode {
|
||||
// Filter tools for custom agents
|
||||
const customAgentTools = useMemo(
|
||||
() => filterToolsForAgent({ tools, isBuiltIn: false, isAsync: false }),
|
||||
[tools],
|
||||
)
|
||||
|
||||
// Expand wildcard or undefined to explicit tool list for internal state
|
||||
const expandedInitialTools =
|
||||
!initialTools || initialTools.includes('*')
|
||||
? customAgentTools.map(t => t.name)
|
||||
: initialTools
|
||||
|
||||
const [selectedTools, setSelectedTools] =
|
||||
useState<string[]>(expandedInitialTools)
|
||||
const [focusIndex, setFocusIndex] = useState(0)
|
||||
const [showIndividualTools, setShowIndividualTools] = useState(false)
|
||||
|
||||
// Filter selectedTools to only include tools that currently exist
|
||||
// This handles MCP tools that disconnect while selected
|
||||
const validSelectedTools = useMemo(() => {
|
||||
const toolNames = new Set(customAgentTools.map(t => t.name))
|
||||
return selectedTools.filter(name => toolNames.has(name))
|
||||
}, [selectedTools, customAgentTools])
|
||||
|
||||
const selectedSet = new Set(validSelectedTools)
|
||||
const isAllSelected =
|
||||
validSelectedTools.length === customAgentTools.length &&
|
||||
customAgentTools.length > 0
|
||||
|
||||
const handleToggleTool = (toolName: string) => {
|
||||
if (!toolName) return
|
||||
|
||||
setSelectedTools(current =>
|
||||
current.includes(toolName)
|
||||
? current.filter(t => t !== toolName)
|
||||
: [...current, toolName],
|
||||
)
|
||||
}
|
||||
const customAgentTools = t1;
|
||||
let t2;
|
||||
if ($[2] !== customAgentTools || $[3] !== initialTools) {
|
||||
t2 = !initialTools || initialTools.includes("*") ? customAgentTools.map(_temp) : initialTools;
|
||||
$[2] = customAgentTools;
|
||||
$[3] = initialTools;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const expandedInitialTools = t2;
|
||||
const [selectedTools, setSelectedTools] = useState(expandedInitialTools);
|
||||
const [focusIndex, setFocusIndex] = useState(0);
|
||||
const [showIndividualTools, setShowIndividualTools] = useState(false);
|
||||
let t3;
|
||||
if ($[5] !== customAgentTools) {
|
||||
t3 = new Set(customAgentTools.map(_temp2));
|
||||
$[5] = customAgentTools;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
const toolNames = t3;
|
||||
let t4;
|
||||
if ($[7] !== selectedTools || $[8] !== toolNames) {
|
||||
let t5;
|
||||
if ($[10] !== toolNames) {
|
||||
t5 = name => toolNames.has(name);
|
||||
$[10] = toolNames;
|
||||
$[11] = t5;
|
||||
} else {
|
||||
t5 = $[11];
|
||||
}
|
||||
t4 = selectedTools.filter(t5);
|
||||
$[7] = selectedTools;
|
||||
$[8] = toolNames;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
const validSelectedTools = t4;
|
||||
let t5;
|
||||
if ($[12] !== validSelectedTools) {
|
||||
t5 = new Set(validSelectedTools);
|
||||
$[12] = validSelectedTools;
|
||||
$[13] = t5;
|
||||
} else {
|
||||
t5 = $[13];
|
||||
}
|
||||
const selectedSet = t5;
|
||||
const isAllSelected = validSelectedTools.length === customAgentTools.length && customAgentTools.length > 0;
|
||||
let t6;
|
||||
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = toolName => {
|
||||
if (!toolName) {
|
||||
return;
|
||||
|
||||
const handleToggleTools = (toolNames: string[], select: boolean) => {
|
||||
setSelectedTools(current => {
|
||||
if (select) {
|
||||
const toolsToAdd = toolNames.filter(t => !current.includes(t))
|
||||
return [...current, ...toolsToAdd]
|
||||
} else {
|
||||
return current.filter(t => !toolNames.includes(t))
|
||||
}
|
||||
setSelectedTools(current => current.includes(toolName) ? current.filter(t_1 => t_1 !== toolName) : [...current, toolName]);
|
||||
};
|
||||
$[14] = t6;
|
||||
} else {
|
||||
t6 = $[14];
|
||||
})
|
||||
}
|
||||
const handleToggleTool = t6;
|
||||
let t7;
|
||||
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = (toolNames_0, select) => {
|
||||
setSelectedTools(current_0 => {
|
||||
if (select) {
|
||||
const toolsToAdd = toolNames_0.filter(t_2 => !current_0.includes(t_2));
|
||||
return [...current_0, ...toolsToAdd];
|
||||
} else {
|
||||
return current_0.filter(t_3 => !toolNames_0.includes(t_3));
|
||||
}
|
||||
});
|
||||
};
|
||||
$[15] = t7;
|
||||
} else {
|
||||
t7 = $[15];
|
||||
|
||||
const handleConfirm = () => {
|
||||
// Convert to undefined if all tools are selected (for cleaner file format)
|
||||
const allToolNames = customAgentTools.map(t => t.name)
|
||||
const areAllToolsSelected =
|
||||
validSelectedTools.length === allToolNames.length &&
|
||||
allToolNames.every(name => validSelectedTools.includes(name))
|
||||
const finalTools = areAllToolsSelected ? undefined : validSelectedTools
|
||||
|
||||
onComplete(finalTools)
|
||||
}
|
||||
const handleToggleTools = t7;
|
||||
let t8;
|
||||
if ($[16] !== customAgentTools || $[17] !== onComplete || $[18] !== validSelectedTools) {
|
||||
t8 = () => {
|
||||
const allToolNames = customAgentTools.map(_temp3);
|
||||
const areAllToolsSelected = validSelectedTools.length === allToolNames.length && allToolNames.every(name_0 => validSelectedTools.includes(name_0));
|
||||
const finalTools = areAllToolsSelected ? undefined : validSelectedTools;
|
||||
onComplete(finalTools);
|
||||
};
|
||||
$[16] = customAgentTools;
|
||||
$[17] = onComplete;
|
||||
$[18] = validSelectedTools;
|
||||
$[19] = t8;
|
||||
} else {
|
||||
t8 = $[19];
|
||||
}
|
||||
const handleConfirm = t8;
|
||||
let buckets;
|
||||
if ($[20] !== customAgentTools) {
|
||||
const toolBuckets = getToolBuckets();
|
||||
buckets = {
|
||||
|
||||
// Group tools by bucket
|
||||
const toolsByBucket = useMemo(() => {
|
||||
const toolBuckets = getToolBuckets()
|
||||
const buckets = {
|
||||
readOnly: [] as Tool[],
|
||||
edit: [] as Tool[],
|
||||
execution: [] as Tool[],
|
||||
mcp: [] as Tool[],
|
||||
other: [] as Tool[]
|
||||
};
|
||||
other: [] as Tool[],
|
||||
}
|
||||
|
||||
customAgentTools.forEach(tool => {
|
||||
// Check if it's an MCP tool first
|
||||
if (isMcpTool(tool)) {
|
||||
buckets.mcp.push(tool);
|
||||
} else {
|
||||
if (toolBuckets.READ_ONLY.toolNames.has(tool.name)) {
|
||||
buckets.readOnly.push(tool);
|
||||
} else {
|
||||
if (toolBuckets.EDIT.toolNames.has(tool.name)) {
|
||||
buckets.edit.push(tool);
|
||||
} else {
|
||||
if (toolBuckets.EXECUTION.toolNames.has(tool.name)) {
|
||||
buckets.execution.push(tool);
|
||||
} else {
|
||||
if (tool.name !== AGENT_TOOL_NAME) {
|
||||
buckets.other.push(tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
buckets.mcp.push(tool)
|
||||
} else if (toolBuckets.READ_ONLY.toolNames.has(tool.name)) {
|
||||
buckets.readOnly.push(tool)
|
||||
} else if (toolBuckets.EDIT.toolNames.has(tool.name)) {
|
||||
buckets.edit.push(tool)
|
||||
} else if (toolBuckets.EXECUTION.toolNames.has(tool.name)) {
|
||||
buckets.execution.push(tool)
|
||||
} else if (tool.name !== AGENT_TOOL_NAME) {
|
||||
// Catch-all for uncategorized tools (except Task)
|
||||
buckets.other.push(tool)
|
||||
}
|
||||
});
|
||||
$[20] = customAgentTools;
|
||||
$[21] = buckets;
|
||||
} else {
|
||||
buckets = $[21];
|
||||
}
|
||||
const toolsByBucket = buckets;
|
||||
let t9;
|
||||
if ($[22] !== selectedSet) {
|
||||
t9 = bucketTools => {
|
||||
const selected = count(bucketTools, (t_5: Tool) => selectedSet.has(t_5.name));
|
||||
const needsSelection = selected < bucketTools.length;
|
||||
return () => {
|
||||
const toolNames_1 = bucketTools.map(_temp4);
|
||||
handleToggleTools(toolNames_1, needsSelection);
|
||||
};
|
||||
};
|
||||
$[22] = selectedSet;
|
||||
$[23] = t9;
|
||||
} else {
|
||||
t9 = $[23];
|
||||
}
|
||||
const createBucketToggleAction = t9;
|
||||
let navigableItems;
|
||||
if ($[24] !== createBucketToggleAction || $[25] !== customAgentTools || $[26] !== focusIndex || $[27] !== handleConfirm || $[28] !== isAllSelected || $[29] !== selectedSet || $[30] !== showIndividualTools || $[31] !== toolsByBucket.edit || $[32] !== toolsByBucket.execution || $[33] !== toolsByBucket.mcp || $[34] !== toolsByBucket.other || $[35] !== toolsByBucket.readOnly) {
|
||||
navigableItems = [];
|
||||
navigableItems.push({
|
||||
id: "continue",
|
||||
label: "Continue",
|
||||
action: handleConfirm,
|
||||
isContinue: true
|
||||
});
|
||||
let t10;
|
||||
if ($[37] !== customAgentTools || $[38] !== isAllSelected) {
|
||||
t10 = () => {
|
||||
const allToolNames_0 = customAgentTools.map(_temp5);
|
||||
handleToggleTools(allToolNames_0, !isAllSelected);
|
||||
};
|
||||
$[37] = customAgentTools;
|
||||
$[38] = isAllSelected;
|
||||
$[39] = t10;
|
||||
} else {
|
||||
t10 = $[39];
|
||||
})
|
||||
|
||||
return buckets
|
||||
}, [customAgentTools])
|
||||
|
||||
const createBucketToggleAction = (bucketTools: Tool[]) => {
|
||||
const selected = count(bucketTools, t => selectedSet.has(t.name))
|
||||
const needsSelection = selected < bucketTools.length
|
||||
|
||||
return () => {
|
||||
const toolNames = bucketTools.map(t => t.name)
|
||||
handleToggleTools(toolNames, needsSelection)
|
||||
}
|
||||
}
|
||||
|
||||
// Build navigable items (no separators)
|
||||
const navigableItems: Array<{
|
||||
id: string
|
||||
label: string
|
||||
action: () => void
|
||||
isContinue?: boolean
|
||||
isToggle?: boolean
|
||||
isHeader?: boolean
|
||||
}> = []
|
||||
|
||||
// Continue button
|
||||
navigableItems.push({
|
||||
id: 'continue',
|
||||
label: 'Continue',
|
||||
action: handleConfirm,
|
||||
isContinue: true,
|
||||
})
|
||||
|
||||
// All tools
|
||||
navigableItems.push({
|
||||
id: 'bucket-all',
|
||||
label: `${isAllSelected ? figures.checkboxOn : figures.checkboxOff} All tools`,
|
||||
action: () => {
|
||||
const allToolNames = customAgentTools.map(t => t.name)
|
||||
handleToggleTools(allToolNames, !isAllSelected)
|
||||
},
|
||||
})
|
||||
|
||||
// Create bucket menu items
|
||||
const toolBuckets = getToolBuckets()
|
||||
const bucketConfigs = [
|
||||
{
|
||||
id: 'bucket-readonly',
|
||||
name: toolBuckets.READ_ONLY.name,
|
||||
tools: toolsByBucket.readOnly,
|
||||
},
|
||||
{
|
||||
id: 'bucket-edit',
|
||||
name: toolBuckets.EDIT.name,
|
||||
tools: toolsByBucket.edit,
|
||||
},
|
||||
{
|
||||
id: 'bucket-execution',
|
||||
name: toolBuckets.EXECUTION.name,
|
||||
tools: toolsByBucket.execution,
|
||||
},
|
||||
{
|
||||
id: 'bucket-mcp',
|
||||
name: toolBuckets.MCP.name,
|
||||
tools: toolsByBucket.mcp,
|
||||
},
|
||||
{
|
||||
id: 'bucket-other',
|
||||
name: toolBuckets.OTHER.name,
|
||||
tools: toolsByBucket.other,
|
||||
},
|
||||
]
|
||||
|
||||
bucketConfigs.forEach(({ id, name, tools: bucketTools }) => {
|
||||
if (bucketTools.length === 0) return
|
||||
|
||||
const selected = count(bucketTools, t => selectedSet.has(t.name))
|
||||
const isFullySelected = selected === bucketTools.length
|
||||
|
||||
navigableItems.push({
|
||||
id: "bucket-all",
|
||||
label: `${isAllSelected ? figures.checkboxOn : figures.checkboxOff} All tools`,
|
||||
action: t10
|
||||
});
|
||||
const toolBuckets_0 = getToolBuckets();
|
||||
const bucketConfigs = [{
|
||||
id: "bucket-readonly",
|
||||
name: toolBuckets_0.READ_ONLY.name,
|
||||
tools: toolsByBucket.readOnly
|
||||
}, {
|
||||
id: "bucket-edit",
|
||||
name: toolBuckets_0.EDIT.name,
|
||||
tools: toolsByBucket.edit
|
||||
}, {
|
||||
id: "bucket-execution",
|
||||
name: toolBuckets_0.EXECUTION.name,
|
||||
tools: toolsByBucket.execution
|
||||
}, {
|
||||
id: "bucket-mcp",
|
||||
name: toolBuckets_0.MCP.name,
|
||||
tools: toolsByBucket.mcp
|
||||
}, {
|
||||
id: "bucket-other",
|
||||
name: toolBuckets_0.OTHER.name,
|
||||
tools: toolsByBucket.other
|
||||
}];
|
||||
bucketConfigs.forEach(t11 => {
|
||||
const {
|
||||
id,
|
||||
name: name_1,
|
||||
tools: bucketTools_0
|
||||
} = t11;
|
||||
if (bucketTools_0.length === 0) {
|
||||
return;
|
||||
id,
|
||||
label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${name}`,
|
||||
action: createBucketToggleAction(bucketTools),
|
||||
})
|
||||
})
|
||||
|
||||
// Toggle button for individual tools
|
||||
const toggleButtonIndex = navigableItems.length
|
||||
navigableItems.push({
|
||||
id: 'toggle-individual',
|
||||
label: showIndividualTools
|
||||
? 'Hide advanced options'
|
||||
: 'Show advanced options',
|
||||
action: () => {
|
||||
setShowIndividualTools(!showIndividualTools)
|
||||
// If hiding tools and focus is on an individual tool, move focus to toggle button
|
||||
if (showIndividualTools && focusIndex > toggleButtonIndex) {
|
||||
setFocusIndex(toggleButtonIndex)
|
||||
}
|
||||
const selected_0 = count(bucketTools_0, (t_8: Tool) => selectedSet.has(t_8.name));
|
||||
const isFullySelected = selected_0 === bucketTools_0.length;
|
||||
},
|
||||
isToggle: true,
|
||||
})
|
||||
|
||||
// Memoize MCP server buckets (must be outside conditional for hooks rules)
|
||||
const mcpServerBuckets = useMemo(
|
||||
() => getMcpServerBuckets(customAgentTools),
|
||||
[customAgentTools],
|
||||
)
|
||||
|
||||
// Individual tools (only if expanded)
|
||||
if (showIndividualTools) {
|
||||
// Add MCP server buckets if any exist
|
||||
if (mcpServerBuckets.length > 0) {
|
||||
navigableItems.push({
|
||||
id,
|
||||
label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${name_1}`,
|
||||
action: createBucketToggleAction(bucketTools_0)
|
||||
});
|
||||
});
|
||||
const toggleButtonIndex = navigableItems.length;
|
||||
let t12;
|
||||
if ($[40] !== focusIndex || $[41] !== showIndividualTools || $[42] !== toggleButtonIndex) {
|
||||
t12 = () => {
|
||||
setShowIndividualTools(!showIndividualTools);
|
||||
if (showIndividualTools && focusIndex > toggleButtonIndex) {
|
||||
setFocusIndex(toggleButtonIndex);
|
||||
}
|
||||
};
|
||||
$[40] = focusIndex;
|
||||
$[41] = showIndividualTools;
|
||||
$[42] = toggleButtonIndex;
|
||||
$[43] = t12;
|
||||
id: 'mcp-servers-header',
|
||||
label: 'MCP Servers:',
|
||||
action: () => {}, // No action - just a header
|
||||
isHeader: true,
|
||||
})
|
||||
|
||||
mcpServerBuckets.forEach(({ serverName, tools: serverTools }) => {
|
||||
const selected = count(serverTools, t => selectedSet.has(t.name))
|
||||
const isFullySelected = selected === serverTools.length
|
||||
|
||||
navigableItems.push({
|
||||
id: `mcp-server-${serverName}`,
|
||||
label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${serverName} (${serverTools.length} ${plural(serverTools.length, 'tool')})`,
|
||||
action: () => {
|
||||
const toolNames = serverTools.map(t => t.name)
|
||||
handleToggleTools(toolNames, !isFullySelected)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// Add separator header before individual tools
|
||||
navigableItems.push({
|
||||
id: 'tools-header',
|
||||
label: 'Individual Tools:',
|
||||
action: () => {},
|
||||
isHeader: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Add individual tools
|
||||
customAgentTools.forEach(tool => {
|
||||
let displayName = tool.name
|
||||
if (tool.name.startsWith('mcp__')) {
|
||||
const mcpInfo = mcpInfoFromString(tool.name)
|
||||
displayName = mcpInfo
|
||||
? `${mcpInfo.toolName} (${mcpInfo.serverName})`
|
||||
: tool.name
|
||||
}
|
||||
|
||||
navigableItems.push({
|
||||
id: `tool-${tool.name}`,
|
||||
label: `${selectedSet.has(tool.name) ? figures.checkboxOn : figures.checkboxOff} ${displayName}`,
|
||||
action: () => handleToggleTool(tool.name),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
if (onCancel) {
|
||||
onCancel()
|
||||
} else {
|
||||
t12 = $[43];
|
||||
onComplete(initialTools)
|
||||
}
|
||||
navigableItems.push({
|
||||
id: "toggle-individual",
|
||||
label: showIndividualTools ? "Hide advanced options" : "Show advanced options",
|
||||
action: t12,
|
||||
isToggle: true
|
||||
});
|
||||
const mcpServerBuckets = getMcpServerBuckets(customAgentTools);
|
||||
if (showIndividualTools) {
|
||||
if (mcpServerBuckets.length > 0) {
|
||||
navigableItems.push({
|
||||
id: "mcp-servers-header",
|
||||
label: "MCP Servers:",
|
||||
action: _temp6,
|
||||
isHeader: true
|
||||
});
|
||||
mcpServerBuckets.forEach(t13 => {
|
||||
const {
|
||||
serverName,
|
||||
tools: serverTools
|
||||
} = t13;
|
||||
const selected_1 = count(serverTools, t_9 => selectedSet.has(t_9.name));
|
||||
const isFullySelected_0 = selected_1 === serverTools.length;
|
||||
navigableItems.push({
|
||||
id: `mcp-server-${serverName}`,
|
||||
label: `${isFullySelected_0 ? figures.checkboxOn : figures.checkboxOff} ${serverName} (${serverTools.length} ${plural(serverTools.length, "tool")})`,
|
||||
action: () => {
|
||||
const toolNames_2 = serverTools.map(_temp7);
|
||||
handleToggleTools(toolNames_2, !isFullySelected_0);
|
||||
}
|
||||
});
|
||||
});
|
||||
navigableItems.push({
|
||||
id: "tools-header",
|
||||
label: "Individual Tools:",
|
||||
action: _temp8,
|
||||
isHeader: true
|
||||
});
|
||||
}, [onCancel, onComplete, initialTools])
|
||||
|
||||
useKeybinding('confirm:no', handleCancel, { context: 'Confirmation' })
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'return') {
|
||||
e.preventDefault()
|
||||
const item = navigableItems[focusIndex]
|
||||
if (item && !item.isHeader) {
|
||||
item.action()
|
||||
}
|
||||
customAgentTools.forEach(tool_0 => {
|
||||
let displayName = tool_0.name;
|
||||
if (tool_0.name.startsWith("mcp__")) {
|
||||
const mcpInfo = mcpInfoFromString(tool_0.name);
|
||||
displayName = mcpInfo ? `${mcpInfo.toolName} (${mcpInfo.serverName})` : tool_0.name;
|
||||
}
|
||||
navigableItems.push({
|
||||
id: `tool-${tool_0.name}`,
|
||||
label: `${selectedSet.has(tool_0.name) ? figures.checkboxOn : figures.checkboxOff} ${displayName}`,
|
||||
action: () => handleToggleTool(tool_0.name)
|
||||
});
|
||||
});
|
||||
} else if (e.key === 'up') {
|
||||
e.preventDefault()
|
||||
let newIndex = focusIndex - 1
|
||||
// Skip headers when navigating up
|
||||
while (newIndex > 0 && navigableItems[newIndex]?.isHeader) {
|
||||
newIndex--
|
||||
}
|
||||
setFocusIndex(Math.max(0, newIndex))
|
||||
} else if (e.key === 'down') {
|
||||
e.preventDefault()
|
||||
let newIndex = focusIndex + 1
|
||||
// Skip headers when navigating down
|
||||
while (
|
||||
newIndex < navigableItems.length - 1 &&
|
||||
navigableItems[newIndex]?.isHeader
|
||||
) {
|
||||
newIndex++
|
||||
}
|
||||
setFocusIndex(Math.min(navigableItems.length - 1, newIndex))
|
||||
}
|
||||
$[24] = createBucketToggleAction;
|
||||
$[25] = customAgentTools;
|
||||
$[26] = focusIndex;
|
||||
$[27] = handleConfirm;
|
||||
$[28] = isAllSelected;
|
||||
$[29] = selectedSet;
|
||||
$[30] = showIndividualTools;
|
||||
$[31] = toolsByBucket.edit;
|
||||
$[32] = toolsByBucket.execution;
|
||||
$[33] = toolsByBucket.mcp;
|
||||
$[34] = toolsByBucket.other;
|
||||
$[35] = toolsByBucket.readOnly;
|
||||
$[36] = navigableItems;
|
||||
} else {
|
||||
navigableItems = $[36];
|
||||
}
|
||||
let t10;
|
||||
if ($[44] !== initialTools || $[45] !== onCancel || $[46] !== onComplete) {
|
||||
t10 = () => {
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
} else {
|
||||
onComplete(initialTools);
|
||||
}
|
||||
};
|
||||
$[44] = initialTools;
|
||||
$[45] = onCancel;
|
||||
$[46] = onComplete;
|
||||
$[47] = t10;
|
||||
} else {
|
||||
t10 = $[47];
|
||||
}
|
||||
const handleCancel = t10;
|
||||
let t11;
|
||||
if ($[48] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t11 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[48] = t11;
|
||||
} else {
|
||||
t11 = $[48];
|
||||
}
|
||||
useKeybinding("confirm:no", handleCancel, t11);
|
||||
let t12;
|
||||
if ($[49] !== focusIndex || $[50] !== navigableItems) {
|
||||
t12 = e => {
|
||||
if (e.key === "return") {
|
||||
e.preventDefault();
|
||||
const item = navigableItems[focusIndex];
|
||||
if (item && !item.isHeader) {
|
||||
item.action();
|
||||
}
|
||||
} else {
|
||||
if (e.key === "up") {
|
||||
e.preventDefault();
|
||||
let newIndex = focusIndex - 1;
|
||||
while (newIndex > 0 && navigableItems[newIndex]?.isHeader) {
|
||||
newIndex--;
|
||||
}
|
||||
setFocusIndex(Math.max(0, newIndex));
|
||||
} else {
|
||||
if (e.key === "down") {
|
||||
e.preventDefault();
|
||||
let newIndex_0 = focusIndex + 1;
|
||||
while (newIndex_0 < navigableItems.length - 1 && navigableItems[newIndex_0]?.isHeader) {
|
||||
newIndex_0++;
|
||||
}
|
||||
setFocusIndex(Math.min(navigableItems.length - 1, newIndex_0));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
$[49] = focusIndex;
|
||||
$[50] = navigableItems;
|
||||
$[51] = t12;
|
||||
} else {
|
||||
t12 = $[51];
|
||||
}
|
||||
const handleKeyDown = t12;
|
||||
const t13 = focusIndex === 0 ? "suggestion" : undefined;
|
||||
const t14 = focusIndex === 0;
|
||||
const t15 = focusIndex === 0 ? `${figures.pointer} ` : " ";
|
||||
let t16;
|
||||
if ($[52] !== t13 || $[53] !== t14 || $[54] !== t15) {
|
||||
t16 = <Text color={t13} bold={t14}>{t15}[ Continue ]</Text>;
|
||||
$[52] = t13;
|
||||
$[53] = t14;
|
||||
$[54] = t15;
|
||||
$[55] = t16;
|
||||
} else {
|
||||
t16 = $[55];
|
||||
}
|
||||
let t17;
|
||||
if ($[56] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t17 = <Divider width={40} />;
|
||||
$[56] = t17;
|
||||
} else {
|
||||
t17 = $[56];
|
||||
}
|
||||
let t18;
|
||||
if ($[57] !== navigableItems) {
|
||||
t18 = navigableItems.slice(1);
|
||||
$[57] = navigableItems;
|
||||
$[58] = t18;
|
||||
} else {
|
||||
t18 = $[58];
|
||||
}
|
||||
let t19;
|
||||
if ($[59] !== focusIndex || $[60] !== t18) {
|
||||
t19 = t18.map((item_0, index) => {
|
||||
const isCurrentlyFocused = index + 1 === focusIndex;
|
||||
const isToggleButton = item_0.isToggle;
|
||||
const isHeader = item_0.isHeader;
|
||||
return <React.Fragment key={item_0.id}>{isToggleButton && <Divider width={40} />}{isHeader && index > 0 && <Box marginTop={1} />}<Text color={isHeader ? undefined : isCurrentlyFocused ? "suggestion" : undefined} dimColor={isHeader} bold={isToggleButton && isCurrentlyFocused}>{isHeader ? "" : isCurrentlyFocused ? `${figures.pointer} ` : " "}{isToggleButton ? `[ ${item_0.label} ]` : item_0.label}</Text></React.Fragment>;
|
||||
});
|
||||
$[59] = focusIndex;
|
||||
$[60] = t18;
|
||||
$[61] = t19;
|
||||
} else {
|
||||
t19 = $[61];
|
||||
}
|
||||
const t20 = isAllSelected ? "All tools selected" : `${selectedSet.size} of ${customAgentTools.length} tools selected`;
|
||||
let t21;
|
||||
if ($[62] !== t20) {
|
||||
t21 = <Box marginTop={1} flexDirection="column"><Text dimColor={true}>{t20}</Text></Box>;
|
||||
$[62] = t20;
|
||||
$[63] = t21;
|
||||
} else {
|
||||
t21 = $[63];
|
||||
}
|
||||
let t22;
|
||||
if ($[64] !== handleKeyDown || $[65] !== t16 || $[66] !== t19 || $[67] !== t21) {
|
||||
t22 = <Box flexDirection="column" marginTop={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t16}{t17}{t19}{t21}</Box>;
|
||||
$[64] = handleKeyDown;
|
||||
$[65] = t16;
|
||||
$[66] = t19;
|
||||
$[67] = t21;
|
||||
$[68] = t22;
|
||||
} else {
|
||||
t22 = $[68];
|
||||
}
|
||||
return t22;
|
||||
}
|
||||
function _temp8() {}
|
||||
function _temp7(t_10) {
|
||||
return t_10.name;
|
||||
}
|
||||
function _temp6() {}
|
||||
function _temp5(t_7) {
|
||||
return t_7.name;
|
||||
}
|
||||
function _temp4(t_6) {
|
||||
return t_6.name;
|
||||
}
|
||||
function _temp3(t_4) {
|
||||
return t_4.name;
|
||||
}
|
||||
function _temp2(t_0) {
|
||||
return t_0.name;
|
||||
}
|
||||
function _temp(t) {
|
||||
return t.name;
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* Render Continue button */}
|
||||
<Text
|
||||
color={focusIndex === 0 ? 'suggestion' : undefined}
|
||||
bold={focusIndex === 0}
|
||||
>
|
||||
{focusIndex === 0 ? `${figures.pointer} ` : ' '}[ Continue ]
|
||||
</Text>
|
||||
|
||||
{/* Separator */}
|
||||
<Divider width={40} />
|
||||
|
||||
{/* Render all navigable items except Continue (which is at index 0) */}
|
||||
{navigableItems.slice(1).map((item, index) => {
|
||||
const isCurrentlyFocused = index + 1 === focusIndex
|
||||
const isToggleButton = item.isToggle
|
||||
const isHeader = item.isHeader
|
||||
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
{/* Add separator before toggle button */}
|
||||
{isToggleButton && <Divider width={40} />}
|
||||
|
||||
{/* Add margin before headers */}
|
||||
{isHeader && index > 0 && <Box marginTop={1} />}
|
||||
|
||||
<Text
|
||||
color={
|
||||
isHeader
|
||||
? undefined
|
||||
: isCurrentlyFocused
|
||||
? 'suggestion'
|
||||
: undefined
|
||||
}
|
||||
dimColor={isHeader}
|
||||
bold={isToggleButton && isCurrentlyFocused}
|
||||
>
|
||||
{isHeader
|
||||
? ''
|
||||
: isCurrentlyFocused
|
||||
? `${figures.pointer} `
|
||||
: ' '}
|
||||
{isToggleButton ? `[ ${item.label} ]` : item.label}
|
||||
</Text>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text dimColor>
|
||||
{isAllSelected
|
||||
? 'All tools selected'
|
||||
: `${selectedSet.size} of ${customAgentTools.length} tools selected`}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,96 +1,68 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { isAutoMemoryEnabled } from '../../../memdir/paths.js';
|
||||
import type { Tools } from '../../../Tool.js';
|
||||
import type { AgentDefinition } from '../../../tools/AgentTool/loadAgentsDir.js';
|
||||
import { WizardProvider } from '../../wizard/index.js';
|
||||
import type { WizardStepComponent } from '../../wizard/types.js';
|
||||
import type { AgentWizardData } from './types.js';
|
||||
import { ColorStep } from './wizard-steps/ColorStep.js';
|
||||
import { ConfirmStepWrapper } from './wizard-steps/ConfirmStepWrapper.js';
|
||||
import { DescriptionStep } from './wizard-steps/DescriptionStep.js';
|
||||
import { GenerateStep } from './wizard-steps/GenerateStep.js';
|
||||
import { LocationStep } from './wizard-steps/LocationStep.js';
|
||||
import { MemoryStep } from './wizard-steps/MemoryStep.js';
|
||||
import { MethodStep } from './wizard-steps/MethodStep.js';
|
||||
import { ModelStep } from './wizard-steps/ModelStep.js';
|
||||
import { PromptStep } from './wizard-steps/PromptStep.js';
|
||||
import { ToolsStep } from './wizard-steps/ToolsStep.js';
|
||||
import { TypeStep } from './wizard-steps/TypeStep.js';
|
||||
import React, { type ReactNode } from 'react'
|
||||
import { isAutoMemoryEnabled } from '../../../memdir/paths.js'
|
||||
import type { Tools } from '../../../Tool.js'
|
||||
import type { AgentDefinition } from '../../../tools/AgentTool/loadAgentsDir.js'
|
||||
import { WizardProvider } from '../../wizard/index.js'
|
||||
import type { WizardStepComponent } from '../../wizard/types.js'
|
||||
import type { AgentWizardData } from './types.js'
|
||||
import { ColorStep } from './wizard-steps/ColorStep.js'
|
||||
import { ConfirmStepWrapper } from './wizard-steps/ConfirmStepWrapper.js'
|
||||
import { DescriptionStep } from './wizard-steps/DescriptionStep.js'
|
||||
import { GenerateStep } from './wizard-steps/GenerateStep.js'
|
||||
import { LocationStep } from './wizard-steps/LocationStep.js'
|
||||
import { MemoryStep } from './wizard-steps/MemoryStep.js'
|
||||
import { MethodStep } from './wizard-steps/MethodStep.js'
|
||||
import { ModelStep } from './wizard-steps/ModelStep.js'
|
||||
import { PromptStep } from './wizard-steps/PromptStep.js'
|
||||
import { ToolsStep } from './wizard-steps/ToolsStep.js'
|
||||
import { TypeStep } from './wizard-steps/TypeStep.js'
|
||||
|
||||
type Props = {
|
||||
tools: Tools;
|
||||
existingAgents: AgentDefinition[];
|
||||
onComplete: (message: string) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
export function CreateAgentWizard(t0) {
|
||||
const $ = _c(17);
|
||||
const {
|
||||
tools,
|
||||
existingAgents,
|
||||
onComplete,
|
||||
onCancel
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== existingAgents) {
|
||||
t1 = () => <TypeStep existingAgents={existingAgents} />;
|
||||
$[0] = existingAgents;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== tools) {
|
||||
t2 = () => <ToolsStep tools={tools} />;
|
||||
$[2] = tools;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = isAutoMemoryEnabled() ? [MemoryStep] : [];
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== existingAgents || $[6] !== onComplete || $[7] !== tools) {
|
||||
t4 = () => <ConfirmStepWrapper tools={tools} existingAgents={existingAgents} onComplete={onComplete} />;
|
||||
$[5] = existingAgents;
|
||||
$[6] = onComplete;
|
||||
$[7] = tools;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
let t5;
|
||||
if ($[9] !== t1 || $[10] !== t2 || $[11] !== t4) {
|
||||
t5 = [LocationStep, MethodStep, GenerateStep, t1, PromptStep, DescriptionStep, t2, ModelStep, ColorStep, ...t3, t4];
|
||||
$[9] = t1;
|
||||
$[10] = t2;
|
||||
$[11] = t4;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
const steps = t5;
|
||||
let t6;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = {};
|
||||
$[13] = t6;
|
||||
} else {
|
||||
t6 = $[13];
|
||||
}
|
||||
let t7;
|
||||
if ($[14] !== onCancel || $[15] !== steps) {
|
||||
t7 = <WizardProvider steps={steps} initialData={t6} onComplete={_temp} onCancel={onCancel} title="Create new agent" showStepCounter={false} />;
|
||||
$[14] = onCancel;
|
||||
$[15] = steps;
|
||||
$[16] = t7;
|
||||
} else {
|
||||
t7 = $[16];
|
||||
}
|
||||
return t7;
|
||||
tools: Tools
|
||||
existingAgents: AgentDefinition[]
|
||||
onComplete: (message: string) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function CreateAgentWizard({
|
||||
tools,
|
||||
existingAgents,
|
||||
onComplete,
|
||||
onCancel,
|
||||
}: Props): ReactNode {
|
||||
// Create step components with props
|
||||
const steps: WizardStepComponent<AgentWizardData>[] = [
|
||||
LocationStep, // 0
|
||||
MethodStep, // 1
|
||||
GenerateStep, // 2
|
||||
() => <TypeStep existingAgents={existingAgents} />, // 3
|
||||
PromptStep, // 4
|
||||
DescriptionStep, // 5
|
||||
() => <ToolsStep tools={tools} />, // 6
|
||||
ModelStep, // 7
|
||||
ColorStep, // 8
|
||||
// MemoryStep is conditionally included based on GrowthBook gate
|
||||
...(isAutoMemoryEnabled() ? [MemoryStep] : []),
|
||||
() => (
|
||||
<ConfirmStepWrapper
|
||||
tools={tools}
|
||||
existingAgents={existingAgents}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
),
|
||||
]
|
||||
|
||||
return (
|
||||
<WizardProvider<AgentWizardData>
|
||||
steps={steps}
|
||||
initialData={{}}
|
||||
onComplete={() => {
|
||||
// Wizard completion is handled by ConfirmStepWrapper
|
||||
// which calls onComplete with the appropriate message
|
||||
}}
|
||||
onCancel={onCancel}
|
||||
title="Create new agent"
|
||||
showStepCounter={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
function _temp() {}
|
||||
|
||||
@@ -1,83 +1,64 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { Box } from '../../../../ink.js';
|
||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
|
||||
import type { AgentColorName } from '../../../../tools/AgentTool/agentColorManager.js';
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../../design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
||||
import { useWizard } from '../../../wizard/index.js';
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
||||
import { ColorPicker } from '../../ColorPicker.js';
|
||||
import type { AgentWizardData } from '../types.js';
|
||||
export function ColorStep() {
|
||||
const $ = _c(14);
|
||||
const {
|
||||
goNext,
|
||||
goBack,
|
||||
updateWizardData,
|
||||
wizardData
|
||||
} = useWizard();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
import React, { type ReactNode } from 'react'
|
||||
import { Box } from '../../../../ink.js'
|
||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
|
||||
import type { AgentColorName } from '../../../../tools/AgentTool/agentColorManager.js'
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||
import { Byline } from '../../../design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||
import { useWizard } from '../../../wizard/index.js'
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||
import { ColorPicker } from '../../ColorPicker.js'
|
||||
import type { AgentWizardData } from '../types.js'
|
||||
|
||||
export function ColorStep(): ReactNode {
|
||||
const { goNext, goBack, updateWizardData, wizardData } =
|
||||
useWizard<AgentWizardData>()
|
||||
|
||||
// Handle escape key - ColorPicker handles its own escape internally
|
||||
useKeybinding('confirm:no', goBack, { context: 'Confirmation' })
|
||||
|
||||
const handleConfirm = (color?: string): void => {
|
||||
updateWizardData({
|
||||
selectedColor: color,
|
||||
// Prepare final agent for confirmation
|
||||
finalAgent: {
|
||||
agentType: wizardData.agentType!,
|
||||
whenToUse: wizardData.whenToUse!,
|
||||
getSystemPrompt: () => wizardData.systemPrompt!,
|
||||
tools: wizardData.selectedTools,
|
||||
...(wizardData.selectedModel
|
||||
? { model: wizardData.selectedModel }
|
||||
: {}),
|
||||
...(color ? { color: color as AgentColorName } : {}),
|
||||
source: wizardData.location!,
|
||||
},
|
||||
})
|
||||
goNext()
|
||||
}
|
||||
useKeybinding("confirm:no", goBack, t0);
|
||||
let t1;
|
||||
if ($[1] !== goNext || $[2] !== updateWizardData || $[3] !== wizardData.agentType || $[4] !== wizardData.location || $[5] !== wizardData.selectedModel || $[6] !== wizardData.selectedTools || $[7] !== wizardData.systemPrompt || $[8] !== wizardData.whenToUse) {
|
||||
t1 = color => {
|
||||
updateWizardData({
|
||||
selectedColor: color,
|
||||
finalAgent: {
|
||||
agentType: wizardData.agentType,
|
||||
whenToUse: wizardData.whenToUse,
|
||||
getSystemPrompt: () => wizardData.systemPrompt,
|
||||
tools: wizardData.selectedTools,
|
||||
...(wizardData.selectedModel ? {
|
||||
model: wizardData.selectedModel
|
||||
} : {}),
|
||||
...(color ? {
|
||||
color: color as AgentColorName
|
||||
} : {}),
|
||||
source: wizardData.location
|
||||
}
|
||||
});
|
||||
goNext();
|
||||
};
|
||||
$[1] = goNext;
|
||||
$[2] = updateWizardData;
|
||||
$[3] = wizardData.agentType;
|
||||
$[4] = wizardData.location;
|
||||
$[5] = wizardData.selectedModel;
|
||||
$[6] = wizardData.selectedTools;
|
||||
$[7] = wizardData.systemPrompt;
|
||||
$[8] = wizardData.whenToUse;
|
||||
$[9] = t1;
|
||||
} else {
|
||||
t1 = $[9];
|
||||
}
|
||||
const handleConfirm = t1;
|
||||
let t2;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
|
||||
$[10] = t2;
|
||||
} else {
|
||||
t2 = $[10];
|
||||
}
|
||||
const t3 = wizardData.agentType || "agent";
|
||||
let t4;
|
||||
if ($[11] !== handleConfirm || $[12] !== t3) {
|
||||
t4 = <WizardDialogLayout subtitle="Choose background color" footerText={t2}><Box><ColorPicker agentName={t3} currentColor="automatic" onConfirm={handleConfirm} /></Box></WizardDialogLayout>;
|
||||
$[11] = handleConfirm;
|
||||
$[12] = t3;
|
||||
$[13] = t4;
|
||||
} else {
|
||||
t4 = $[13];
|
||||
}
|
||||
return t4;
|
||||
|
||||
return (
|
||||
<WizardDialogLayout
|
||||
subtitle="Choose background color"
|
||||
footerText={
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="go back"
|
||||
/>
|
||||
</Byline>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<ColorPicker
|
||||
agentName={wizardData.agentType || 'agent'}
|
||||
currentColor="automatic"
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
</Box>
|
||||
</WizardDialogLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,377 +1,168 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { type ReactNode } from 'react';
|
||||
import type { KeyboardEvent } from '../../../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../../../ink.js';
|
||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
|
||||
import { isAutoMemoryEnabled } from '../../../../memdir/paths.js';
|
||||
import type { Tools } from '../../../../Tool.js';
|
||||
import { getMemoryScopeDisplay } from '../../../../tools/AgentTool/agentMemory.js';
|
||||
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js';
|
||||
import { truncateToWidth } from '../../../../utils/format.js';
|
||||
import { getAgentModelDisplay } from '../../../../utils/model/agent.js';
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../../design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
||||
import { useWizard } from '../../../wizard/index.js';
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
||||
import { getNewRelativeAgentFilePath } from '../../agentFileUtils.js';
|
||||
import { validateAgent } from '../../validateAgent.js';
|
||||
import type { AgentWizardData } from '../types.js';
|
||||
import React, { type ReactNode } from 'react'
|
||||
import type { KeyboardEvent } from '../../../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../../../ink.js'
|
||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
|
||||
import { isAutoMemoryEnabled } from '../../../../memdir/paths.js'
|
||||
import type { Tools } from '../../../../Tool.js'
|
||||
import { getMemoryScopeDisplay } from '../../../../tools/AgentTool/agentMemory.js'
|
||||
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'
|
||||
import { truncateToWidth } from '../../../../utils/format.js'
|
||||
import { getAgentModelDisplay } from '../../../../utils/model/agent.js'
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||
import { Byline } from '../../../design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||
import { useWizard } from '../../../wizard/index.js'
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||
import { getNewRelativeAgentFilePath } from '../../agentFileUtils.js'
|
||||
import { validateAgent } from '../../validateAgent.js'
|
||||
import type { AgentWizardData } from '../types.js'
|
||||
|
||||
type Props = {
|
||||
tools: Tools;
|
||||
existingAgents: AgentDefinition[];
|
||||
onSave: () => void;
|
||||
onSaveAndEdit: () => void;
|
||||
error?: string | null;
|
||||
};
|
||||
export function ConfirmStep(t0) {
|
||||
const $ = _c(88);
|
||||
const {
|
||||
tools,
|
||||
existingAgents,
|
||||
onSave,
|
||||
onSaveAndEdit,
|
||||
error
|
||||
} = t0;
|
||||
const {
|
||||
goBack,
|
||||
wizardData
|
||||
} = useWizard();
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
tools: Tools
|
||||
existingAgents: AgentDefinition[]
|
||||
onSave: () => void
|
||||
onSaveAndEdit: () => void
|
||||
error?: string | null
|
||||
}
|
||||
|
||||
export function ConfirmStep({
|
||||
tools,
|
||||
existingAgents,
|
||||
onSave,
|
||||
onSaveAndEdit,
|
||||
error,
|
||||
}: Props): ReactNode {
|
||||
const { goBack, wizardData } = useWizard<AgentWizardData>()
|
||||
|
||||
useKeybinding('confirm:no', goBack, { context: 'Confirmation' })
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 's' || e.key === 'return') {
|
||||
e.preventDefault()
|
||||
onSave()
|
||||
} else if (e.key === 'e') {
|
||||
e.preventDefault()
|
||||
onSaveAndEdit()
|
||||
}
|
||||
}
|
||||
useKeybinding("confirm:no", goBack, t1);
|
||||
let t2;
|
||||
if ($[1] !== onSave || $[2] !== onSaveAndEdit) {
|
||||
t2 = e => {
|
||||
if (e.key === "s" || e.key === "return") {
|
||||
e.preventDefault();
|
||||
onSave();
|
||||
} else {
|
||||
if (e.key === "e") {
|
||||
e.preventDefault();
|
||||
onSaveAndEdit();
|
||||
}
|
||||
|
||||
const agent = wizardData.finalAgent!
|
||||
const validation = validateAgent(agent, tools, existingAgents)
|
||||
|
||||
const systemPromptPreview = truncateToWidth(agent.getSystemPrompt(), 240)
|
||||
const whenToUsePreview = truncateToWidth(agent.whenToUse, 240)
|
||||
|
||||
const getToolsDisplay = (toolNames: string[] | undefined): string => {
|
||||
// undefined means "all tools" per PR semantic
|
||||
if (toolNames === undefined) return 'All tools'
|
||||
if (toolNames.length === 0) return 'None'
|
||||
if (toolNames.length === 1) return toolNames[0] || 'None'
|
||||
if (toolNames.length === 2) return toolNames.join(' and ')
|
||||
return `${toolNames.slice(0, -1).join(', ')}, and ${toolNames[toolNames.length - 1]}`
|
||||
}
|
||||
|
||||
// Compute memory display outside JSX
|
||||
const memoryDisplayElement = isAutoMemoryEnabled() ? (
|
||||
<Text>
|
||||
<Text bold>Memory</Text>: {getMemoryScopeDisplay(agent.memory)}
|
||||
</Text>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<WizardDialogLayout
|
||||
subtitle="Confirm and save"
|
||||
footerText={
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="s/Enter" action="save" />
|
||||
<KeyboardShortcutHint shortcut="e" action="edit in your editor" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="cancel"
|
||||
/>
|
||||
</Byline>
|
||||
}
|
||||
};
|
||||
$[1] = onSave;
|
||||
$[2] = onSaveAndEdit;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const handleKeyDown = t2;
|
||||
const agent = wizardData.finalAgent;
|
||||
let T0;
|
||||
let T1;
|
||||
let t10;
|
||||
let t11;
|
||||
let t12;
|
||||
let t13;
|
||||
let t14;
|
||||
let t15;
|
||||
let t16;
|
||||
let t17;
|
||||
let t18;
|
||||
let t19;
|
||||
let t3;
|
||||
let t4;
|
||||
let t5;
|
||||
let t6;
|
||||
let t7;
|
||||
let t8;
|
||||
let t9;
|
||||
if ($[4] !== agent || $[5] !== existingAgents || $[6] !== handleKeyDown || $[7] !== tools || $[8] !== wizardData.location) {
|
||||
const validation = validateAgent(agent, tools, existingAgents);
|
||||
let t20;
|
||||
if ($[28] !== agent) {
|
||||
t20 = truncateToWidth(agent.getSystemPrompt(), 240);
|
||||
$[28] = agent;
|
||||
$[29] = t20;
|
||||
} else {
|
||||
t20 = $[29];
|
||||
}
|
||||
const systemPromptPreview = t20;
|
||||
let t21;
|
||||
if ($[30] !== agent.whenToUse) {
|
||||
t21 = truncateToWidth(agent.whenToUse, 240);
|
||||
$[30] = agent.whenToUse;
|
||||
$[31] = t21;
|
||||
} else {
|
||||
t21 = $[31];
|
||||
}
|
||||
const whenToUsePreview = t21;
|
||||
const getToolsDisplay = _temp;
|
||||
let t22;
|
||||
if ($[32] !== agent.memory) {
|
||||
t22 = isAutoMemoryEnabled() ? <Text><Text bold={true}>Memory</Text>: {getMemoryScopeDisplay(agent.memory)}</Text> : null;
|
||||
$[32] = agent.memory;
|
||||
$[33] = t22;
|
||||
} else {
|
||||
t22 = $[33];
|
||||
}
|
||||
const memoryDisplayElement = t22;
|
||||
T1 = WizardDialogLayout;
|
||||
t18 = "Confirm and save";
|
||||
if ($[34] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t19 = <Byline><KeyboardShortcutHint shortcut="s/Enter" action="save" /><KeyboardShortcutHint shortcut="e" action="edit in your editor" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /></Byline>;
|
||||
$[34] = t19;
|
||||
} else {
|
||||
t19 = $[34];
|
||||
}
|
||||
T0 = Box;
|
||||
t3 = "column";
|
||||
t4 = 0;
|
||||
t5 = true;
|
||||
t6 = handleKeyDown;
|
||||
let t23;
|
||||
if ($[35] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t23 = <Text bold={true}>Name</Text>;
|
||||
$[35] = t23;
|
||||
} else {
|
||||
t23 = $[35];
|
||||
}
|
||||
if ($[36] !== agent.agentType) {
|
||||
t7 = <Text>{t23}: {agent.agentType}</Text>;
|
||||
$[36] = agent.agentType;
|
||||
$[37] = t7;
|
||||
} else {
|
||||
t7 = $[37];
|
||||
}
|
||||
let t24;
|
||||
if ($[38] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t24 = <Text bold={true}>Location</Text>;
|
||||
$[38] = t24;
|
||||
} else {
|
||||
t24 = $[38];
|
||||
}
|
||||
let t25;
|
||||
if ($[39] !== agent.agentType || $[40] !== wizardData.location) {
|
||||
t25 = getNewRelativeAgentFilePath({
|
||||
source: wizardData.location,
|
||||
agentType: agent.agentType
|
||||
});
|
||||
$[39] = agent.agentType;
|
||||
$[40] = wizardData.location;
|
||||
$[41] = t25;
|
||||
} else {
|
||||
t25 = $[41];
|
||||
}
|
||||
if ($[42] !== t25) {
|
||||
t8 = <Text>{t24}:{" "}{t25}</Text>;
|
||||
$[42] = t25;
|
||||
$[43] = t8;
|
||||
} else {
|
||||
t8 = $[43];
|
||||
}
|
||||
let t26;
|
||||
if ($[44] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t26 = <Text bold={true}>Tools</Text>;
|
||||
$[44] = t26;
|
||||
} else {
|
||||
t26 = $[44];
|
||||
}
|
||||
let t27;
|
||||
if ($[45] !== agent.tools) {
|
||||
t27 = getToolsDisplay(agent.tools);
|
||||
$[45] = agent.tools;
|
||||
$[46] = t27;
|
||||
} else {
|
||||
t27 = $[46];
|
||||
}
|
||||
if ($[47] !== t27) {
|
||||
t9 = <Text>{t26}: {t27}</Text>;
|
||||
$[47] = t27;
|
||||
$[48] = t9;
|
||||
} else {
|
||||
t9 = $[48];
|
||||
}
|
||||
let t28;
|
||||
if ($[49] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t28 = <Text bold={true}>Model</Text>;
|
||||
$[49] = t28;
|
||||
} else {
|
||||
t28 = $[49];
|
||||
}
|
||||
let t29;
|
||||
if ($[50] !== agent.model) {
|
||||
t29 = getAgentModelDisplay(agent.model);
|
||||
$[50] = agent.model;
|
||||
$[51] = t29;
|
||||
} else {
|
||||
t29 = $[51];
|
||||
}
|
||||
if ($[52] !== t29) {
|
||||
t10 = <Text>{t28}: {t29}</Text>;
|
||||
$[52] = t29;
|
||||
$[53] = t10;
|
||||
} else {
|
||||
t10 = $[53];
|
||||
}
|
||||
t11 = memoryDisplayElement;
|
||||
if ($[54] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t12 = <Box marginTop={1}><Text><Text bold={true}>Description</Text> (tells Claude when to use this agent):</Text></Box>;
|
||||
$[54] = t12;
|
||||
} else {
|
||||
t12 = $[54];
|
||||
}
|
||||
if ($[55] !== whenToUsePreview) {
|
||||
t13 = <Box marginLeft={2} marginTop={1}><Text>{whenToUsePreview}</Text></Box>;
|
||||
$[55] = whenToUsePreview;
|
||||
$[56] = t13;
|
||||
} else {
|
||||
t13 = $[56];
|
||||
}
|
||||
if ($[57] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t14 = <Box marginTop={1}><Text><Text bold={true}>System prompt</Text>:</Text></Box>;
|
||||
$[57] = t14;
|
||||
} else {
|
||||
t14 = $[57];
|
||||
}
|
||||
if ($[58] !== systemPromptPreview) {
|
||||
t15 = <Box marginLeft={2} marginTop={1}><Text>{systemPromptPreview}</Text></Box>;
|
||||
$[58] = systemPromptPreview;
|
||||
$[59] = t15;
|
||||
} else {
|
||||
t15 = $[59];
|
||||
}
|
||||
t16 = validation.warnings.length > 0 && <Box marginTop={1} flexDirection="column"><Text color="warning">Warnings:</Text>{validation.warnings.map(_temp2)}</Box>;
|
||||
t17 = validation.errors.length > 0 && <Box marginTop={1} flexDirection="column"><Text color="error">Errors:</Text>{validation.errors.map(_temp3)}</Box>;
|
||||
$[4] = agent;
|
||||
$[5] = existingAgents;
|
||||
$[6] = handleKeyDown;
|
||||
$[7] = tools;
|
||||
$[8] = wizardData.location;
|
||||
$[9] = T0;
|
||||
$[10] = T1;
|
||||
$[11] = t10;
|
||||
$[12] = t11;
|
||||
$[13] = t12;
|
||||
$[14] = t13;
|
||||
$[15] = t14;
|
||||
$[16] = t15;
|
||||
$[17] = t16;
|
||||
$[18] = t17;
|
||||
$[19] = t18;
|
||||
$[20] = t19;
|
||||
$[21] = t3;
|
||||
$[22] = t4;
|
||||
$[23] = t5;
|
||||
$[24] = t6;
|
||||
$[25] = t7;
|
||||
$[26] = t8;
|
||||
$[27] = t9;
|
||||
} else {
|
||||
T0 = $[9];
|
||||
T1 = $[10];
|
||||
t10 = $[11];
|
||||
t11 = $[12];
|
||||
t12 = $[13];
|
||||
t13 = $[14];
|
||||
t14 = $[15];
|
||||
t15 = $[16];
|
||||
t16 = $[17];
|
||||
t17 = $[18];
|
||||
t18 = $[19];
|
||||
t19 = $[20];
|
||||
t3 = $[21];
|
||||
t4 = $[22];
|
||||
t5 = $[23];
|
||||
t6 = $[24];
|
||||
t7 = $[25];
|
||||
t8 = $[26];
|
||||
t9 = $[27];
|
||||
}
|
||||
let t20;
|
||||
if ($[60] !== error) {
|
||||
t20 = error && <Box marginTop={1}><Text color="error">{error}</Text></Box>;
|
||||
$[60] = error;
|
||||
$[61] = t20;
|
||||
} else {
|
||||
t20 = $[61];
|
||||
}
|
||||
let t21;
|
||||
if ($[62] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t21 = <Text bold={true}>s</Text>;
|
||||
$[62] = t21;
|
||||
} else {
|
||||
t21 = $[62];
|
||||
}
|
||||
let t22;
|
||||
if ($[63] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t22 = <Text bold={true}>Enter</Text>;
|
||||
$[63] = t22;
|
||||
} else {
|
||||
t22 = $[63];
|
||||
}
|
||||
let t23;
|
||||
if ($[64] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t23 = <Box marginTop={2}><Text color="success">Press {t21} or {t22} to save,{" "}<Text bold={true}>e</Text> to save and edit</Text></Box>;
|
||||
$[64] = t23;
|
||||
} else {
|
||||
t23 = $[64];
|
||||
}
|
||||
let t24;
|
||||
if ($[65] !== T0 || $[66] !== t10 || $[67] !== t11 || $[68] !== t12 || $[69] !== t13 || $[70] !== t14 || $[71] !== t15 || $[72] !== t16 || $[73] !== t17 || $[74] !== t20 || $[75] !== t3 || $[76] !== t4 || $[77] !== t5 || $[78] !== t6 || $[79] !== t7 || $[80] !== t8 || $[81] !== t9) {
|
||||
t24 = <T0 flexDirection={t3} tabIndex={t4} autoFocus={t5} onKeyDown={t6}>{t7}{t8}{t9}{t10}{t11}{t12}{t13}{t14}{t15}{t16}{t17}{t20}{t23}</T0>;
|
||||
$[65] = T0;
|
||||
$[66] = t10;
|
||||
$[67] = t11;
|
||||
$[68] = t12;
|
||||
$[69] = t13;
|
||||
$[70] = t14;
|
||||
$[71] = t15;
|
||||
$[72] = t16;
|
||||
$[73] = t17;
|
||||
$[74] = t20;
|
||||
$[75] = t3;
|
||||
$[76] = t4;
|
||||
$[77] = t5;
|
||||
$[78] = t6;
|
||||
$[79] = t7;
|
||||
$[80] = t8;
|
||||
$[81] = t9;
|
||||
$[82] = t24;
|
||||
} else {
|
||||
t24 = $[82];
|
||||
}
|
||||
let t25;
|
||||
if ($[83] !== T1 || $[84] !== t18 || $[85] !== t19 || $[86] !== t24) {
|
||||
t25 = <T1 subtitle={t18} footerText={t19}>{t24}</T1>;
|
||||
$[83] = T1;
|
||||
$[84] = t18;
|
||||
$[85] = t19;
|
||||
$[86] = t24;
|
||||
$[87] = t25;
|
||||
} else {
|
||||
t25 = $[87];
|
||||
}
|
||||
return t25;
|
||||
}
|
||||
function _temp3(err, i_0) {
|
||||
return <Text key={i_0} color="error">{" "}• {err}</Text>;
|
||||
}
|
||||
function _temp2(warning, i) {
|
||||
return <Text key={i} dimColor={true}>{" "}• {warning}</Text>;
|
||||
}
|
||||
function _temp(toolNames) {
|
||||
if (toolNames === undefined) {
|
||||
return "All tools";
|
||||
}
|
||||
if (toolNames.length === 0) {
|
||||
return "None";
|
||||
}
|
||||
if (toolNames.length === 1) {
|
||||
return toolNames[0] || "None";
|
||||
}
|
||||
if (toolNames.length === 2) {
|
||||
return toolNames.join(" and ");
|
||||
}
|
||||
return `${toolNames.slice(0, -1).join(", ")}, and ${toolNames[toolNames.length - 1]}`;
|
||||
>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Text>
|
||||
<Text bold>Name</Text>: {agent.agentType}
|
||||
</Text>
|
||||
<Text>
|
||||
<Text bold>Location</Text>:{' '}
|
||||
{getNewRelativeAgentFilePath({
|
||||
source: wizardData.location!,
|
||||
agentType: agent.agentType,
|
||||
})}
|
||||
</Text>
|
||||
<Text>
|
||||
<Text bold>Tools</Text>: {getToolsDisplay(agent.tools)}
|
||||
</Text>
|
||||
<Text>
|
||||
<Text bold>Model</Text>: {getAgentModelDisplay(agent.model)}
|
||||
</Text>
|
||||
{memoryDisplayElement}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text>
|
||||
<Text bold>Description</Text> (tells Claude when to use this agent):
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginLeft={2} marginTop={1}>
|
||||
<Text>{whenToUsePreview}</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text>
|
||||
<Text bold>System prompt</Text>:
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginLeft={2} marginTop={1}>
|
||||
<Text>{systemPromptPreview}</Text>
|
||||
</Box>
|
||||
|
||||
{validation.warnings.length > 0 && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color="warning">Warnings:</Text>
|
||||
{validation.warnings.map((warning, i) => (
|
||||
<Text key={i} dimColor>
|
||||
{' '}
|
||||
• {warning}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{validation.errors.length > 0 && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color="error">Errors:</Text>
|
||||
{validation.errors.map((err, i) => (
|
||||
<Text key={i} color="error">
|
||||
{' '}
|
||||
• {err}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="error">{error}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box marginTop={2}>
|
||||
<Text color="success">
|
||||
Press <Text bold>s</Text> or <Text bold>Enter</Text> to save,{' '}
|
||||
<Text bold>e</Text> to save and edit
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</WizardDialogLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,73 +1,112 @@
|
||||
import chalk from 'chalk';
|
||||
import React, { type ReactNode, useCallback, useState } from 'react';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
||||
import { useSetAppState } from 'src/state/AppState.js';
|
||||
import type { Tools } from '../../../../Tool.js';
|
||||
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js';
|
||||
import { getActiveAgentsFromList } from '../../../../tools/AgentTool/loadAgentsDir.js';
|
||||
import { editFileInEditor } from '../../../../utils/promptEditor.js';
|
||||
import { useWizard } from '../../../wizard/index.js';
|
||||
import { getNewAgentFilePath, saveAgentToFile } from '../../agentFileUtils.js';
|
||||
import type { AgentWizardData } from '../types.js';
|
||||
import { ConfirmStep } from './ConfirmStep.js';
|
||||
import chalk from 'chalk'
|
||||
import React, { type ReactNode, useCallback, useState } from 'react'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { useSetAppState } from 'src/state/AppState.js'
|
||||
import type { Tools } from '../../../../Tool.js'
|
||||
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'
|
||||
import { getActiveAgentsFromList } from '../../../../tools/AgentTool/loadAgentsDir.js'
|
||||
import { editFileInEditor } from '../../../../utils/promptEditor.js'
|
||||
import { useWizard } from '../../../wizard/index.js'
|
||||
import { getNewAgentFilePath, saveAgentToFile } from '../../agentFileUtils.js'
|
||||
import type { AgentWizardData } from '../types.js'
|
||||
import { ConfirmStep } from './ConfirmStep.js'
|
||||
|
||||
type Props = {
|
||||
tools: Tools;
|
||||
existingAgents: AgentDefinition[];
|
||||
onComplete: (message: string) => void;
|
||||
};
|
||||
tools: Tools
|
||||
existingAgents: AgentDefinition[]
|
||||
onComplete: (message: string) => void
|
||||
}
|
||||
|
||||
export function ConfirmStepWrapper({
|
||||
tools,
|
||||
existingAgents,
|
||||
onComplete
|
||||
onComplete,
|
||||
}: Props): ReactNode {
|
||||
const {
|
||||
wizardData
|
||||
} = useWizard<AgentWizardData>();
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const setAppState = useSetAppState();
|
||||
const saveAgent = useCallback(async (openInEditor: boolean): Promise<void> => {
|
||||
if (!wizardData?.finalAgent) return;
|
||||
try {
|
||||
await saveAgentToFile(wizardData.location!, wizardData.finalAgent.agentType, wizardData.finalAgent.whenToUse, wizardData.finalAgent.tools, wizardData.finalAgent.getSystemPrompt(), true, wizardData.finalAgent.color, wizardData.finalAgent.model, wizardData.finalAgent.memory);
|
||||
setAppState(state => {
|
||||
if (!wizardData.finalAgent) return state;
|
||||
const allAgents = state.agentDefinitions.allAgents.concat(wizardData.finalAgent);
|
||||
return {
|
||||
...state,
|
||||
agentDefinitions: {
|
||||
...state.agentDefinitions,
|
||||
activeAgents: getActiveAgentsFromList(allAgents),
|
||||
allAgents
|
||||
const { wizardData } = useWizard<AgentWizardData>()
|
||||
const [saveError, setSaveError] = useState<string | null>(null)
|
||||
const setAppState = useSetAppState()
|
||||
|
||||
const saveAgent = useCallback(
|
||||
async (openInEditor: boolean): Promise<void> => {
|
||||
if (!wizardData?.finalAgent) return
|
||||
|
||||
try {
|
||||
await saveAgentToFile(
|
||||
wizardData.location!,
|
||||
wizardData.finalAgent.agentType,
|
||||
wizardData.finalAgent.whenToUse,
|
||||
wizardData.finalAgent.tools,
|
||||
wizardData.finalAgent.getSystemPrompt(),
|
||||
true,
|
||||
wizardData.finalAgent.color,
|
||||
wizardData.finalAgent.model,
|
||||
wizardData.finalAgent.memory,
|
||||
)
|
||||
|
||||
setAppState(state => {
|
||||
if (!wizardData.finalAgent) return state
|
||||
|
||||
const allAgents = state.agentDefinitions.allAgents.concat(
|
||||
wizardData.finalAgent,
|
||||
)
|
||||
return {
|
||||
...state,
|
||||
agentDefinitions: {
|
||||
...state.agentDefinitions,
|
||||
activeAgents: getActiveAgentsFromList(allAgents),
|
||||
allAgents,
|
||||
},
|
||||
}
|
||||
};
|
||||
});
|
||||
if (openInEditor) {
|
||||
const filePath = getNewAgentFilePath({
|
||||
})
|
||||
|
||||
if (openInEditor) {
|
||||
const filePath = getNewAgentFilePath({
|
||||
source: wizardData.location!,
|
||||
agentType: wizardData.finalAgent.agentType,
|
||||
})
|
||||
await editFileInEditor(filePath)
|
||||
}
|
||||
|
||||
logEvent('tengu_agent_created', {
|
||||
agent_type: wizardData.finalAgent.agentType,
|
||||
generation_method: wizardData.wasGenerated ? 'generated' : 'manual',
|
||||
source: wizardData.location!,
|
||||
agentType: wizardData.finalAgent.agentType
|
||||
});
|
||||
await editFileInEditor(filePath);
|
||||
tool_count: wizardData.finalAgent.tools?.length ?? 'all',
|
||||
has_custom_model: !!wizardData.finalAgent.model,
|
||||
has_custom_color: !!wizardData.finalAgent.color,
|
||||
has_memory: !!wizardData.finalAgent.memory,
|
||||
memory_scope: wizardData.finalAgent.memory ?? 'none',
|
||||
...(openInEditor ? { opened_in_editor: true } : {}),
|
||||
} as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS)
|
||||
|
||||
const message = openInEditor
|
||||
? `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)} and opened in editor. ` +
|
||||
`If you made edits, restart to load the latest version.`
|
||||
: `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)}`
|
||||
onComplete(message)
|
||||
} catch (err) {
|
||||
setSaveError(
|
||||
err instanceof Error ? err.message : 'Failed to save agent',
|
||||
)
|
||||
}
|
||||
logEvent('tengu_agent_created', {
|
||||
agent_type: wizardData.finalAgent.agentType,
|
||||
generation_method: wizardData.wasGenerated ? 'generated' : 'manual',
|
||||
source: wizardData.location!,
|
||||
tool_count: wizardData.finalAgent.tools?.length ?? 'all',
|
||||
has_custom_model: !!wizardData.finalAgent.model,
|
||||
has_custom_color: !!wizardData.finalAgent.color,
|
||||
has_memory: !!wizardData.finalAgent.memory,
|
||||
memory_scope: wizardData.finalAgent.memory ?? 'none',
|
||||
...(openInEditor ? {
|
||||
opened_in_editor: true
|
||||
} : {})
|
||||
} as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS);
|
||||
const message = openInEditor ? `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)} and opened in editor. ` + `If you made edits, restart to load the latest version.` : `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)}`;
|
||||
onComplete(message);
|
||||
} catch (err) {
|
||||
setSaveError(err instanceof Error ? err.message : 'Failed to save agent');
|
||||
}
|
||||
}, [wizardData, onComplete, setAppState]);
|
||||
const handleSave = useCallback(() => saveAgent(false), [saveAgent]);
|
||||
const handleSaveAndEdit = useCallback(() => saveAgent(true), [saveAgent]);
|
||||
return <ConfirmStep tools={tools} existingAgents={existingAgents} onSave={handleSave} onSaveAndEdit={handleSaveAndEdit} error={saveError} />;
|
||||
},
|
||||
[wizardData, onComplete, setAppState],
|
||||
)
|
||||
|
||||
const handleSave = useCallback(() => saveAgent(false), [saveAgent])
|
||||
|
||||
const handleSaveAndEdit = useCallback(() => saveAgent(true), [saveAgent])
|
||||
|
||||
return (
|
||||
<ConfirmStep
|
||||
tools={tools}
|
||||
existingAgents={existingAgents}
|
||||
onSave={handleSave}
|
||||
onSaveAndEdit={handleSaveAndEdit}
|
||||
error={saveError}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,122 +1,94 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { type ReactNode, useCallback, useState } from 'react';
|
||||
import { Box, Text } from '../../../../ink.js';
|
||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
|
||||
import { editPromptInEditor } from '../../../../utils/promptEditor.js';
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../../design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
||||
import TextInput from '../../../TextInput.js';
|
||||
import { useWizard } from '../../../wizard/index.js';
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
||||
import type { AgentWizardData } from '../types.js';
|
||||
export function DescriptionStep() {
|
||||
const $ = _c(18);
|
||||
const {
|
||||
goNext,
|
||||
goBack,
|
||||
updateWizardData,
|
||||
wizardData
|
||||
} = useWizard();
|
||||
const [whenToUse, setWhenToUse] = useState(wizardData.whenToUse || "");
|
||||
const [cursorOffset, setCursorOffset] = useState(whenToUse.length);
|
||||
const [error, setError] = useState(null);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = {
|
||||
context: "Settings"
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
import React, { type ReactNode, useCallback, useState } from 'react'
|
||||
import { Box, Text } from '../../../../ink.js'
|
||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
|
||||
import { editPromptInEditor } from '../../../../utils/promptEditor.js'
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||
import { Byline } from '../../../design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||
import TextInput from '../../../TextInput.js'
|
||||
import { useWizard } from '../../../wizard/index.js'
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||
import type { AgentWizardData } from '../types.js'
|
||||
|
||||
export function DescriptionStep(): ReactNode {
|
||||
const { goNext, goBack, updateWizardData, wizardData } =
|
||||
useWizard<AgentWizardData>()
|
||||
const [whenToUse, setWhenToUse] = useState(wizardData.whenToUse || '')
|
||||
const [cursorOffset, setCursorOffset] = useState(whenToUse.length)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Handle escape key - use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
|
||||
useKeybinding('confirm:no', goBack, { context: 'Settings' })
|
||||
|
||||
const handleExternalEditor = useCallback(async () => {
|
||||
const result = await editPromptInEditor(whenToUse)
|
||||
if (result.content !== null) {
|
||||
setWhenToUse(result.content)
|
||||
setCursorOffset(result.content.length)
|
||||
}
|
||||
}, [whenToUse])
|
||||
|
||||
useKeybinding('chat:externalEditor', handleExternalEditor, {
|
||||
context: 'Chat',
|
||||
})
|
||||
|
||||
const handleSubmit = (value: string): void => {
|
||||
const trimmedValue = value.trim()
|
||||
if (!trimmedValue) {
|
||||
setError('Description is required')
|
||||
return
|
||||
}
|
||||
|
||||
setError(null)
|
||||
updateWizardData({ whenToUse: trimmedValue })
|
||||
goNext()
|
||||
}
|
||||
useKeybinding("confirm:no", goBack, t0);
|
||||
let t1;
|
||||
if ($[1] !== whenToUse) {
|
||||
t1 = async () => {
|
||||
const result = await editPromptInEditor(whenToUse);
|
||||
if (result.content !== null) {
|
||||
setWhenToUse(result.content);
|
||||
setCursorOffset(result.content.length);
|
||||
|
||||
return (
|
||||
<WizardDialogLayout
|
||||
subtitle="Description (tell Claude when to use this agent)"
|
||||
footerText={
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="Type" action="enter text" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="continue" />
|
||||
<ConfigurableShortcutHint
|
||||
action="chat:externalEditor"
|
||||
context="Chat"
|
||||
fallback="ctrl+g"
|
||||
description="open in editor"
|
||||
/>
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Settings"
|
||||
fallback="Esc"
|
||||
description="go back"
|
||||
/>
|
||||
</Byline>
|
||||
}
|
||||
};
|
||||
$[1] = whenToUse;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const handleExternalEditor = t1;
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = {
|
||||
context: "Chat"
|
||||
};
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
useKeybinding("chat:externalEditor", handleExternalEditor, t2);
|
||||
let t3;
|
||||
if ($[4] !== goNext || $[5] !== updateWizardData) {
|
||||
t3 = value => {
|
||||
const trimmedValue = value.trim();
|
||||
if (!trimmedValue) {
|
||||
setError("Description is required");
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
updateWizardData({
|
||||
whenToUse: trimmedValue
|
||||
});
|
||||
goNext();
|
||||
};
|
||||
$[4] = goNext;
|
||||
$[5] = updateWizardData;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
const handleSubmit = t3;
|
||||
let t4;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Byline><KeyboardShortcutHint shortcut="Type" action="enter text" /><KeyboardShortcutHint shortcut="Enter" action="continue" /><ConfigurableShortcutHint action="chat:externalEditor" context="Chat" fallback="ctrl+g" description="open in editor" /><ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="go back" /></Byline>;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text>When should Claude use this agent?</Text>;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
let t6;
|
||||
if ($[9] !== cursorOffset || $[10] !== handleSubmit || $[11] !== whenToUse) {
|
||||
t6 = <Box marginTop={1}><TextInput value={whenToUse} onChange={setWhenToUse} onSubmit={handleSubmit} placeholder="e.g., use this agent after you're done writing code..." columns={80} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus={true} showCursor={true} /></Box>;
|
||||
$[9] = cursorOffset;
|
||||
$[10] = handleSubmit;
|
||||
$[11] = whenToUse;
|
||||
$[12] = t6;
|
||||
} else {
|
||||
t6 = $[12];
|
||||
}
|
||||
let t7;
|
||||
if ($[13] !== error) {
|
||||
t7 = error && <Box marginTop={1}><Text color="error">{error}</Text></Box>;
|
||||
$[13] = error;
|
||||
$[14] = t7;
|
||||
} else {
|
||||
t7 = $[14];
|
||||
}
|
||||
let t8;
|
||||
if ($[15] !== t6 || $[16] !== t7) {
|
||||
t8 = <WizardDialogLayout subtitle="Description (tell Claude when to use this agent)" footerText={t4}><Box flexDirection="column">{t5}{t6}{t7}</Box></WizardDialogLayout>;
|
||||
$[15] = t6;
|
||||
$[16] = t7;
|
||||
$[17] = t8;
|
||||
} else {
|
||||
t8 = $[17];
|
||||
}
|
||||
return t8;
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
<Text>When should Claude use this agent?</Text>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<TextInput
|
||||
value={whenToUse}
|
||||
onChange={setWhenToUse}
|
||||
onSubmit={handleSubmit}
|
||||
placeholder="e.g., use this agent after you're done writing code..."
|
||||
columns={80}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
focus
|
||||
showCursor
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="error">{error}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</WizardDialogLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,58 +1,57 @@
|
||||
import { APIUserAbortError } from '@anthropic-ai/sdk';
|
||||
import React, { type ReactNode, useCallback, useRef, useState } from 'react';
|
||||
import { useMainLoopModel } from '../../../../hooks/useMainLoopModel.js';
|
||||
import { Box, Text } from '../../../../ink.js';
|
||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
|
||||
import { createAbortController } from '../../../../utils/abortController.js';
|
||||
import { editPromptInEditor } from '../../../../utils/promptEditor.js';
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../../design-system/Byline.js';
|
||||
import { Spinner } from '../../../Spinner.js';
|
||||
import TextInput from '../../../TextInput.js';
|
||||
import { useWizard } from '../../../wizard/index.js';
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
||||
import { generateAgent } from '../../generateAgent.js';
|
||||
import type { AgentWizardData } from '../types.js';
|
||||
import { APIUserAbortError } from '@anthropic-ai/sdk'
|
||||
import React, { type ReactNode, useCallback, useRef, useState } from 'react'
|
||||
import { useMainLoopModel } from '../../../../hooks/useMainLoopModel.js'
|
||||
import { Box, Text } from '../../../../ink.js'
|
||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
|
||||
import { createAbortController } from '../../../../utils/abortController.js'
|
||||
import { editPromptInEditor } from '../../../../utils/promptEditor.js'
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||
import { Byline } from '../../../design-system/Byline.js'
|
||||
import { Spinner } from '../../../Spinner.js'
|
||||
import TextInput from '../../../TextInput.js'
|
||||
import { useWizard } from '../../../wizard/index.js'
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||
import { generateAgent } from '../../generateAgent.js'
|
||||
import type { AgentWizardData } from '../types.js'
|
||||
|
||||
export function GenerateStep(): ReactNode {
|
||||
const {
|
||||
updateWizardData,
|
||||
goBack,
|
||||
goToStep,
|
||||
wizardData
|
||||
} = useWizard<AgentWizardData>();
|
||||
const [prompt, setPrompt] = useState(wizardData.generationPrompt || '');
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [cursorOffset, setCursorOffset] = useState(prompt.length);
|
||||
const model = useMainLoopModel();
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const { updateWizardData, goBack, goToStep, wizardData } =
|
||||
useWizard<AgentWizardData>()
|
||||
const [prompt, setPrompt] = useState(wizardData.generationPrompt || '')
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [cursorOffset, setCursorOffset] = useState(prompt.length)
|
||||
const model = useMainLoopModel()
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
|
||||
// Cancel generation when escape pressed during generation
|
||||
const handleCancelGeneration = useCallback(() => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
setIsGenerating(false);
|
||||
setError('Generation cancelled');
|
||||
abortControllerRef.current.abort()
|
||||
abortControllerRef.current = null
|
||||
setIsGenerating(false)
|
||||
setError('Generation cancelled')
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in prompt input)
|
||||
useKeybinding('confirm:no', handleCancelGeneration, {
|
||||
context: 'Settings',
|
||||
isActive: isGenerating
|
||||
});
|
||||
isActive: isGenerating,
|
||||
})
|
||||
|
||||
const handleExternalEditor = useCallback(async () => {
|
||||
const result = await editPromptInEditor(prompt);
|
||||
const result = await editPromptInEditor(prompt)
|
||||
if (result.content !== null) {
|
||||
setPrompt(result.content);
|
||||
setCursorOffset(result.content.length);
|
||||
setPrompt(result.content)
|
||||
setCursorOffset(result.content.length)
|
||||
}
|
||||
}, [prompt]);
|
||||
}, [prompt])
|
||||
|
||||
useKeybinding('chat:externalEditor', handleExternalEditor, {
|
||||
context: 'Chat',
|
||||
isActive: !isGenerating
|
||||
});
|
||||
isActive: !isGenerating,
|
||||
})
|
||||
|
||||
// Go back when escape pressed while not generating
|
||||
const handleGoBack = useCallback(() => {
|
||||
@@ -62,81 +61,141 @@ export function GenerateStep(): ReactNode {
|
||||
systemPrompt: '',
|
||||
whenToUse: '',
|
||||
generatedAgent: undefined,
|
||||
wasGenerated: false
|
||||
});
|
||||
setPrompt('');
|
||||
setError(null);
|
||||
goBack();
|
||||
}, [updateWizardData, goBack]);
|
||||
wasGenerated: false,
|
||||
})
|
||||
setPrompt('')
|
||||
setError(null)
|
||||
goBack()
|
||||
}, [updateWizardData, goBack])
|
||||
|
||||
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in prompt input)
|
||||
useKeybinding('confirm:no', handleGoBack, {
|
||||
context: 'Settings',
|
||||
isActive: !isGenerating
|
||||
});
|
||||
isActive: !isGenerating,
|
||||
})
|
||||
|
||||
const handleGenerate = async (): Promise<void> => {
|
||||
const trimmedPrompt = prompt.trim();
|
||||
const trimmedPrompt = prompt.trim()
|
||||
if (!trimmedPrompt) {
|
||||
setError('Please describe what the agent should do');
|
||||
return;
|
||||
setError('Please describe what the agent should do')
|
||||
return
|
||||
}
|
||||
setError(null);
|
||||
setIsGenerating(true);
|
||||
|
||||
setError(null)
|
||||
setIsGenerating(true)
|
||||
updateWizardData({
|
||||
generationPrompt: trimmedPrompt,
|
||||
isGenerating: true
|
||||
});
|
||||
isGenerating: true,
|
||||
})
|
||||
|
||||
// Create abort controller for this generation
|
||||
const controller = createAbortController();
|
||||
abortControllerRef.current = controller;
|
||||
const controller = createAbortController()
|
||||
abortControllerRef.current = controller
|
||||
|
||||
try {
|
||||
const generated = await generateAgent(trimmedPrompt, model, [], controller.signal);
|
||||
const generated = await generateAgent(
|
||||
trimmedPrompt,
|
||||
model,
|
||||
[],
|
||||
controller.signal,
|
||||
)
|
||||
|
||||
updateWizardData({
|
||||
agentType: generated.identifier,
|
||||
whenToUse: generated.whenToUse,
|
||||
systemPrompt: generated.systemPrompt,
|
||||
generatedAgent: generated,
|
||||
isGenerating: false,
|
||||
wasGenerated: true
|
||||
});
|
||||
wasGenerated: true,
|
||||
})
|
||||
|
||||
// Skip directly to ToolsStep (index 6) - matching original flow
|
||||
goToStep(6);
|
||||
goToStep(6)
|
||||
} catch (err) {
|
||||
// Don't show error if it was cancelled (already set in escape handler)
|
||||
if (err instanceof APIUserAbortError) {
|
||||
// User cancelled - no error to show
|
||||
} else if (err instanceof Error && !err.message.includes('No assistant message found')) {
|
||||
setError(err.message || 'Failed to generate agent');
|
||||
} else if (
|
||||
err instanceof Error &&
|
||||
!err.message.includes('No assistant message found')
|
||||
) {
|
||||
setError(err.message || 'Failed to generate agent')
|
||||
}
|
||||
updateWizardData({
|
||||
isGenerating: false
|
||||
});
|
||||
updateWizardData({ isGenerating: false })
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
abortControllerRef.current = null;
|
||||
setIsGenerating(false)
|
||||
abortControllerRef.current = null
|
||||
}
|
||||
};
|
||||
const subtitle = 'Describe what this agent should do and when it should be used (be comprehensive for best results)';
|
||||
}
|
||||
|
||||
const subtitle =
|
||||
'Describe what this agent should do and when it should be used (be comprehensive for best results)'
|
||||
|
||||
if (isGenerating) {
|
||||
return <WizardDialogLayout subtitle={subtitle} footerText={<ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" />}>
|
||||
return (
|
||||
<WizardDialogLayout
|
||||
subtitle={subtitle}
|
||||
footerText={
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Settings"
|
||||
fallback="Esc"
|
||||
description="cancel"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Box flexDirection="row" alignItems="center">
|
||||
<Spinner />
|
||||
<Text color="suggestion"> Generating agent from description...</Text>
|
||||
</Box>
|
||||
</WizardDialogLayout>;
|
||||
</WizardDialogLayout>
|
||||
)
|
||||
}
|
||||
return <WizardDialogLayout subtitle={subtitle} footerText={<Byline>
|
||||
<ConfigurableShortcutHint action="confirm:yes" context="Confirmation" fallback="Enter" description="submit" />
|
||||
<ConfigurableShortcutHint action="chat:externalEditor" context="Chat" fallback="ctrl+g" description="open in editor" />
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="go back" />
|
||||
</Byline>}>
|
||||
|
||||
return (
|
||||
<WizardDialogLayout
|
||||
subtitle={subtitle}
|
||||
footerText={
|
||||
<Byline>
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:yes"
|
||||
context="Confirmation"
|
||||
fallback="Enter"
|
||||
description="submit"
|
||||
/>
|
||||
<ConfigurableShortcutHint
|
||||
action="chat:externalEditor"
|
||||
context="Chat"
|
||||
fallback="ctrl+g"
|
||||
description="open in editor"
|
||||
/>
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Settings"
|
||||
fallback="Esc"
|
||||
description="go back"
|
||||
/>
|
||||
</Byline>
|
||||
}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
{error && <Box marginBottom={1}>
|
||||
{error && (
|
||||
<Box marginBottom={1}>
|
||||
<Text color="error">{error}</Text>
|
||||
</Box>}
|
||||
<TextInput value={prompt} onChange={setPrompt} onSubmit={handleGenerate} placeholder="e.g., Help me write unit tests for my code..." columns={80} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus showCursor />
|
||||
</Box>
|
||||
)}
|
||||
<TextInput
|
||||
value={prompt}
|
||||
onChange={setPrompt}
|
||||
onSubmit={handleGenerate}
|
||||
placeholder="e.g., Help me write unit tests for my code..."
|
||||
columns={80}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
focus
|
||||
showCursor
|
||||
/>
|
||||
</Box>
|
||||
</WizardDialogLayout>;
|
||||
</WizardDialogLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,79 +1,55 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { Box } from '../../../../ink.js';
|
||||
import type { SettingSource } from '../../../../utils/settings/constants.js';
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
||||
import { Select } from '../../../CustomSelect/select.js';
|
||||
import { Byline } from '../../../design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
||||
import { useWizard } from '../../../wizard/index.js';
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
||||
import type { AgentWizardData } from '../types.js';
|
||||
export function LocationStep() {
|
||||
const $ = _c(11);
|
||||
const {
|
||||
goNext,
|
||||
updateWizardData,
|
||||
cancel
|
||||
} = useWizard();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = {
|
||||
label: "Project (.claude/agents/)",
|
||||
value: "projectSettings" as SettingSource
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = [t0, {
|
||||
label: "Personal (~/.claude/agents/)",
|
||||
value: "userSettings" as SettingSource
|
||||
}];
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const locationOptions = t1;
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /></Byline>;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== goNext || $[4] !== updateWizardData) {
|
||||
t3 = value => {
|
||||
updateWizardData({
|
||||
location: value as SettingSource
|
||||
});
|
||||
goNext();
|
||||
};
|
||||
$[3] = goNext;
|
||||
$[4] = updateWizardData;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== cancel) {
|
||||
t4 = () => cancel();
|
||||
$[6] = cancel;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] !== t3 || $[9] !== t4) {
|
||||
t5 = <WizardDialogLayout subtitle="Choose location" footerText={t2}><Box><Select key="location-select" options={locationOptions} onChange={t3} onCancel={t4} /></Box></WizardDialogLayout>;
|
||||
$[8] = t3;
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
return t5;
|
||||
import React, { type ReactNode } from 'react'
|
||||
import { Box } from '../../../../ink.js'
|
||||
import type { SettingSource } from '../../../../utils/settings/constants.js'
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||
import { Select } from '../../../CustomSelect/select.js'
|
||||
import { Byline } from '../../../design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||
import { useWizard } from '../../../wizard/index.js'
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||
import type { AgentWizardData } from '../types.js'
|
||||
|
||||
export function LocationStep(): ReactNode {
|
||||
const { goNext, updateWizardData, cancel } = useWizard<AgentWizardData>()
|
||||
|
||||
const locationOptions = [
|
||||
{
|
||||
label: 'Project (.claude/agents/)',
|
||||
value: 'projectSettings' as SettingSource,
|
||||
},
|
||||
{
|
||||
label: 'Personal (~/.claude/agents/)',
|
||||
value: 'userSettings' as SettingSource,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<WizardDialogLayout
|
||||
subtitle="Choose location"
|
||||
footerText={
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="cancel"
|
||||
/>
|
||||
</Byline>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Select
|
||||
key="location-select"
|
||||
options={locationOptions}
|
||||
onChange={(value: string) => {
|
||||
updateWizardData({ location: value as SettingSource })
|
||||
goNext()
|
||||
}}
|
||||
onCancel={() => cancel()}
|
||||
/>
|
||||
</Box>
|
||||
</WizardDialogLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,112 +1,102 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { Box } from '../../../../ink.js';
|
||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
|
||||
import { isAutoMemoryEnabled } from '../../../../memdir/paths.js';
|
||||
import { type AgentMemoryScope, loadAgentMemoryPrompt } from '../../../../tools/AgentTool/agentMemory.js';
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
||||
import { Select } from '../../../CustomSelect/select.js';
|
||||
import { Byline } from '../../../design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
||||
import { useWizard } from '../../../wizard/index.js';
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
||||
import type { AgentWizardData } from '../types.js';
|
||||
import React, { type ReactNode } from 'react'
|
||||
import { Box } from '../../../../ink.js'
|
||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
|
||||
import { isAutoMemoryEnabled } from '../../../../memdir/paths.js'
|
||||
import {
|
||||
type AgentMemoryScope,
|
||||
loadAgentMemoryPrompt,
|
||||
} from '../../../../tools/AgentTool/agentMemory.js'
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||
import { Select } from '../../../CustomSelect/select.js'
|
||||
import { Byline } from '../../../design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||
import { useWizard } from '../../../wizard/index.js'
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||
import type { AgentWizardData } from '../types.js'
|
||||
|
||||
type MemoryOption = {
|
||||
label: string;
|
||||
value: AgentMemoryScope | 'none';
|
||||
};
|
||||
export function MemoryStep() {
|
||||
const $ = _c(13);
|
||||
const {
|
||||
goNext,
|
||||
goBack,
|
||||
updateWizardData,
|
||||
wizardData
|
||||
} = useWizard();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
useKeybinding("confirm:no", goBack, t0);
|
||||
const isUserScope = wizardData.location === "userSettings";
|
||||
let t1;
|
||||
if ($[1] !== isUserScope) {
|
||||
t1 = isUserScope ? [{
|
||||
label: "User scope (~/.claude/agent-memory/) (Recommended)",
|
||||
value: "user"
|
||||
}, {
|
||||
label: "None (no persistent memory)",
|
||||
value: "none"
|
||||
}, {
|
||||
label: "Project scope (.claude/agent-memory/)",
|
||||
value: "project"
|
||||
}, {
|
||||
label: "Local scope (.claude/agent-memory-local/)",
|
||||
value: "local"
|
||||
}] : [{
|
||||
label: "Project scope (.claude/agent-memory/) (Recommended)",
|
||||
value: "project"
|
||||
}, {
|
||||
label: "None (no persistent memory)",
|
||||
value: "none"
|
||||
}, {
|
||||
label: "User scope (~/.claude/agent-memory/)",
|
||||
value: "user"
|
||||
}, {
|
||||
label: "Local scope (.claude/agent-memory-local/)",
|
||||
value: "local"
|
||||
}];
|
||||
$[1] = isUserScope;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const memoryOptions = t1;
|
||||
let t2;
|
||||
if ($[3] !== goNext || $[4] !== updateWizardData || $[5] !== wizardData.finalAgent || $[6] !== wizardData.systemPrompt) {
|
||||
t2 = value => {
|
||||
const memory = value === "none" ? undefined : value as AgentMemoryScope;
|
||||
const agentType = wizardData.finalAgent?.agentType;
|
||||
updateWizardData({
|
||||
selectedMemory: memory,
|
||||
finalAgent: wizardData.finalAgent ? {
|
||||
...wizardData.finalAgent,
|
||||
memory,
|
||||
getSystemPrompt: isAutoMemoryEnabled() && memory && agentType ? () => wizardData.systemPrompt + "\n\n" + loadAgentMemoryPrompt(agentType, memory) : () => wizardData.systemPrompt
|
||||
} : undefined
|
||||
});
|
||||
goNext();
|
||||
};
|
||||
$[3] = goNext;
|
||||
$[4] = updateWizardData;
|
||||
$[5] = wizardData.finalAgent;
|
||||
$[6] = wizardData.systemPrompt;
|
||||
$[7] = t2;
|
||||
} else {
|
||||
t2 = $[7];
|
||||
}
|
||||
const handleSelect = t2;
|
||||
let t3;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
let t4;
|
||||
if ($[9] !== goBack || $[10] !== handleSelect || $[11] !== memoryOptions) {
|
||||
t4 = <WizardDialogLayout subtitle="Configure agent memory" footerText={t3}><Box><Select key="memory-select" options={memoryOptions} onChange={handleSelect} onCancel={goBack} /></Box></WizardDialogLayout>;
|
||||
$[9] = goBack;
|
||||
$[10] = handleSelect;
|
||||
$[11] = memoryOptions;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
}
|
||||
return t4;
|
||||
label: string
|
||||
value: AgentMemoryScope | 'none'
|
||||
}
|
||||
|
||||
export function MemoryStep(): ReactNode {
|
||||
const { goNext, goBack, updateWizardData, wizardData } =
|
||||
useWizard<AgentWizardData>()
|
||||
|
||||
useKeybinding('confirm:no', goBack, { context: 'Confirmation' })
|
||||
|
||||
const isUserScope = wizardData.location === 'userSettings'
|
||||
|
||||
// Build options with the recommended default first, then alternatives
|
||||
// The recommended scope matches the agent's location (project agent → project memory, user agent → user memory)
|
||||
const memoryOptions: MemoryOption[] = isUserScope
|
||||
? [
|
||||
{
|
||||
label: 'User scope (~/.claude/agent-memory/) (Recommended)',
|
||||
value: 'user',
|
||||
},
|
||||
{ label: 'None (no persistent memory)', value: 'none' },
|
||||
{ label: 'Project scope (.claude/agent-memory/)', value: 'project' },
|
||||
{ label: 'Local scope (.claude/agent-memory-local/)', value: 'local' },
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: 'Project scope (.claude/agent-memory/) (Recommended)',
|
||||
value: 'project',
|
||||
},
|
||||
{ label: 'None (no persistent memory)', value: 'none' },
|
||||
{ label: 'User scope (~/.claude/agent-memory/)', value: 'user' },
|
||||
{ label: 'Local scope (.claude/agent-memory-local/)', value: 'local' },
|
||||
]
|
||||
|
||||
const handleSelect = (value: string): void => {
|
||||
const memory = value === 'none' ? undefined : (value as AgentMemoryScope)
|
||||
const agentType = wizardData.finalAgent?.agentType
|
||||
updateWizardData({
|
||||
selectedMemory: memory,
|
||||
// Update finalAgent with memory and rewire getSystemPrompt to include memory loading.
|
||||
// Explicitly set memory (not conditional spread) so selecting 'none' after going back clears it.
|
||||
finalAgent: wizardData.finalAgent
|
||||
? {
|
||||
...wizardData.finalAgent,
|
||||
memory,
|
||||
getSystemPrompt:
|
||||
isAutoMemoryEnabled() && memory && agentType
|
||||
? () =>
|
||||
wizardData.systemPrompt! +
|
||||
'\n\n' +
|
||||
loadAgentMemoryPrompt(agentType, memory)
|
||||
: () => wizardData.systemPrompt!,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
goNext()
|
||||
}
|
||||
|
||||
return (
|
||||
<WizardDialogLayout
|
||||
subtitle="Configure agent memory"
|
||||
footerText={
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="go back"
|
||||
/>
|
||||
</Byline>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Select
|
||||
key="memory-select"
|
||||
options={memoryOptions}
|
||||
onChange={handleSelect}
|
||||
onCancel={goBack}
|
||||
/>
|
||||
</Box>
|
||||
</WizardDialogLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,79 +1,65 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { Box } from '../../../../ink.js';
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
||||
import { Select } from '../../../CustomSelect/select.js';
|
||||
import { Byline } from '../../../design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
||||
import { useWizard } from '../../../wizard/index.js';
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
||||
import type { AgentWizardData } from '../types.js';
|
||||
export function MethodStep() {
|
||||
const $ = _c(11);
|
||||
const {
|
||||
goNext,
|
||||
goBack,
|
||||
updateWizardData,
|
||||
goToStep
|
||||
} = useWizard();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = [{
|
||||
label: "Generate with Claude (recommended)",
|
||||
value: "generate"
|
||||
}, {
|
||||
label: "Manual configuration",
|
||||
value: "manual"
|
||||
}];
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const methodOptions = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== goNext || $[3] !== goToStep || $[4] !== updateWizardData) {
|
||||
t2 = value => {
|
||||
const method = value as 'generate' | 'manual';
|
||||
updateWizardData({
|
||||
method,
|
||||
wasGenerated: method === "generate"
|
||||
});
|
||||
if (method === "generate") {
|
||||
goNext();
|
||||
} else {
|
||||
goToStep(3);
|
||||
import React, { type ReactNode } from 'react'
|
||||
import { Box } from '../../../../ink.js'
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||
import { Select } from '../../../CustomSelect/select.js'
|
||||
import { Byline } from '../../../design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||
import { useWizard } from '../../../wizard/index.js'
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||
import type { AgentWizardData } from '../types.js'
|
||||
|
||||
export function MethodStep(): ReactNode {
|
||||
const { goNext, goBack, updateWizardData, goToStep } =
|
||||
useWizard<AgentWizardData>()
|
||||
|
||||
const methodOptions = [
|
||||
{
|
||||
label: 'Generate with Claude (recommended)',
|
||||
value: 'generate',
|
||||
},
|
||||
{
|
||||
label: 'Manual configuration',
|
||||
value: 'manual',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<WizardDialogLayout
|
||||
subtitle="Creation method"
|
||||
footerText={
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="go back"
|
||||
/>
|
||||
</Byline>
|
||||
}
|
||||
};
|
||||
$[2] = goNext;
|
||||
$[3] = goToStep;
|
||||
$[4] = updateWizardData;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
let t3;
|
||||
if ($[6] !== goBack) {
|
||||
t3 = () => goBack();
|
||||
$[6] = goBack;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
let t4;
|
||||
if ($[8] !== t2 || $[9] !== t3) {
|
||||
t4 = <WizardDialogLayout subtitle="Creation method" footerText={t1}><Box><Select key="method-select" options={methodOptions} onChange={t2} onCancel={t3} /></Box></WizardDialogLayout>;
|
||||
$[8] = t2;
|
||||
$[9] = t3;
|
||||
$[10] = t4;
|
||||
} else {
|
||||
t4 = $[10];
|
||||
}
|
||||
return t4;
|
||||
>
|
||||
<Box>
|
||||
<Select
|
||||
key="method-select"
|
||||
options={methodOptions}
|
||||
onChange={(value: string) => {
|
||||
const method = value as 'generate' | 'manual'
|
||||
updateWizardData({
|
||||
method,
|
||||
wasGenerated: method === 'generate',
|
||||
})
|
||||
|
||||
// Dynamic navigation based on method
|
||||
if (method === 'generate') {
|
||||
goNext() // Go to GenerateStep (index 2)
|
||||
} else {
|
||||
goToStep(3) // Skip to TypeStep (index 3)
|
||||
}
|
||||
}}
|
||||
onCancel={() => goBack()}
|
||||
/>
|
||||
</Box>
|
||||
</WizardDialogLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,51 +1,42 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../../design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
||||
import { useWizard } from '../../../wizard/index.js';
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
||||
import { ModelSelector } from '../../ModelSelector.js';
|
||||
import type { AgentWizardData } from '../types.js';
|
||||
export function ModelStep() {
|
||||
const $ = _c(8);
|
||||
const {
|
||||
goNext,
|
||||
goBack,
|
||||
updateWizardData,
|
||||
wizardData
|
||||
} = useWizard();
|
||||
let t0;
|
||||
if ($[0] !== goNext || $[1] !== updateWizardData) {
|
||||
t0 = model => {
|
||||
updateWizardData({
|
||||
selectedModel: model
|
||||
});
|
||||
goNext();
|
||||
};
|
||||
$[0] = goNext;
|
||||
$[1] = updateWizardData;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
import React, { type ReactNode } from 'react'
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||
import { Byline } from '../../../design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||
import { useWizard } from '../../../wizard/index.js'
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||
import { ModelSelector } from '../../ModelSelector.js'
|
||||
import type { AgentWizardData } from '../types.js'
|
||||
|
||||
export function ModelStep(): ReactNode {
|
||||
const { goNext, goBack, updateWizardData, wizardData } =
|
||||
useWizard<AgentWizardData>()
|
||||
|
||||
const handleComplete = (model?: string): void => {
|
||||
updateWizardData({ selectedModel: model })
|
||||
goNext()
|
||||
}
|
||||
const handleComplete = t0;
|
||||
let t1;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== goBack || $[5] !== handleComplete || $[6] !== wizardData.selectedModel) {
|
||||
t2 = <WizardDialogLayout subtitle="Select model" footerText={t1}><ModelSelector initialModel={wizardData.selectedModel} onComplete={handleComplete} onCancel={goBack} /></WizardDialogLayout>;
|
||||
$[4] = goBack;
|
||||
$[5] = handleComplete;
|
||||
$[6] = wizardData.selectedModel;
|
||||
$[7] = t2;
|
||||
} else {
|
||||
t2 = $[7];
|
||||
}
|
||||
return t2;
|
||||
|
||||
return (
|
||||
<WizardDialogLayout
|
||||
subtitle="Select model"
|
||||
footerText={
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="go back"
|
||||
/>
|
||||
</Byline>
|
||||
}
|
||||
>
|
||||
<ModelSelector
|
||||
initialModel={wizardData.selectedModel}
|
||||
onComplete={handleComplete}
|
||||
onCancel={goBack}
|
||||
/>
|
||||
</WizardDialogLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,127 +1,97 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { type ReactNode, useCallback, useState } from 'react';
|
||||
import { Box, Text } from '../../../../ink.js';
|
||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
|
||||
import { editPromptInEditor } from '../../../../utils/promptEditor.js';
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../../design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
||||
import TextInput from '../../../TextInput.js';
|
||||
import { useWizard } from '../../../wizard/index.js';
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
||||
import type { AgentWizardData } from '../types.js';
|
||||
export function PromptStep() {
|
||||
const $ = _c(20);
|
||||
const {
|
||||
goNext,
|
||||
goBack,
|
||||
updateWizardData,
|
||||
wizardData
|
||||
} = useWizard();
|
||||
const [systemPrompt, setSystemPrompt] = useState(wizardData.systemPrompt || "");
|
||||
const [cursorOffset, setCursorOffset] = useState(systemPrompt.length);
|
||||
const [error, setError] = useState(null);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = {
|
||||
context: "Settings"
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
import React, { type ReactNode, useCallback, useState } from 'react'
|
||||
import { Box, Text } from '../../../../ink.js'
|
||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
|
||||
import { editPromptInEditor } from '../../../../utils/promptEditor.js'
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||
import { Byline } from '../../../design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||
import TextInput from '../../../TextInput.js'
|
||||
import { useWizard } from '../../../wizard/index.js'
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||
import type { AgentWizardData } from '../types.js'
|
||||
|
||||
export function PromptStep(): ReactNode {
|
||||
const { goNext, goBack, updateWizardData, wizardData } =
|
||||
useWizard<AgentWizardData>()
|
||||
const [systemPrompt, setSystemPrompt] = useState(
|
||||
wizardData.systemPrompt || '',
|
||||
)
|
||||
const [cursorOffset, setCursorOffset] = useState(systemPrompt.length)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Handle escape key - use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
|
||||
useKeybinding('confirm:no', goBack, { context: 'Settings' })
|
||||
|
||||
const handleExternalEditor = useCallback(async () => {
|
||||
const result = await editPromptInEditor(systemPrompt)
|
||||
if (result.content !== null) {
|
||||
setSystemPrompt(result.content)
|
||||
setCursorOffset(result.content.length)
|
||||
}
|
||||
}, [systemPrompt])
|
||||
|
||||
useKeybinding('chat:externalEditor', handleExternalEditor, {
|
||||
context: 'Chat',
|
||||
})
|
||||
|
||||
const handleSubmit = (): void => {
|
||||
const trimmedPrompt = systemPrompt.trim()
|
||||
if (!trimmedPrompt) {
|
||||
setError('System prompt is required')
|
||||
return
|
||||
}
|
||||
|
||||
setError(null)
|
||||
updateWizardData({ systemPrompt: trimmedPrompt })
|
||||
goNext()
|
||||
}
|
||||
useKeybinding("confirm:no", goBack, t0);
|
||||
let t1;
|
||||
if ($[1] !== systemPrompt) {
|
||||
t1 = async () => {
|
||||
const result = await editPromptInEditor(systemPrompt);
|
||||
if (result.content !== null) {
|
||||
setSystemPrompt(result.content);
|
||||
setCursorOffset(result.content.length);
|
||||
|
||||
return (
|
||||
<WizardDialogLayout
|
||||
subtitle="System prompt"
|
||||
footerText={
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="Type" action="enter text" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="continue" />
|
||||
<ConfigurableShortcutHint
|
||||
action="chat:externalEditor"
|
||||
context="Chat"
|
||||
fallback="ctrl+g"
|
||||
description="open in editor"
|
||||
/>
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Settings"
|
||||
fallback="Esc"
|
||||
description="go back"
|
||||
/>
|
||||
</Byline>
|
||||
}
|
||||
};
|
||||
$[1] = systemPrompt;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const handleExternalEditor = t1;
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = {
|
||||
context: "Chat"
|
||||
};
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
useKeybinding("chat:externalEditor", handleExternalEditor, t2);
|
||||
let t3;
|
||||
if ($[4] !== goNext || $[5] !== systemPrompt || $[6] !== updateWizardData) {
|
||||
t3 = () => {
|
||||
const trimmedPrompt = systemPrompt.trim();
|
||||
if (!trimmedPrompt) {
|
||||
setError("System prompt is required");
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
updateWizardData({
|
||||
systemPrompt: trimmedPrompt
|
||||
});
|
||||
goNext();
|
||||
};
|
||||
$[4] = goNext;
|
||||
$[5] = systemPrompt;
|
||||
$[6] = updateWizardData;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
const handleSubmit = t3;
|
||||
let t4;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Byline><KeyboardShortcutHint shortcut="Type" action="enter text" /><KeyboardShortcutHint shortcut="Enter" action="continue" /><ConfigurableShortcutHint action="chat:externalEditor" context="Chat" fallback="ctrl+g" description="open in editor" /><ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="go back" /></Byline>;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
let t5;
|
||||
let t6;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text>Enter the system prompt for your agent:</Text>;
|
||||
t6 = <Text dimColor={true}>Be comprehensive for best results</Text>;
|
||||
$[9] = t5;
|
||||
$[10] = t6;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
t6 = $[10];
|
||||
}
|
||||
let t7;
|
||||
if ($[11] !== cursorOffset || $[12] !== handleSubmit || $[13] !== systemPrompt) {
|
||||
t7 = <Box marginTop={1}><TextInput value={systemPrompt} onChange={setSystemPrompt} onSubmit={handleSubmit} placeholder="You are a helpful code reviewer who..." columns={80} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus={true} showCursor={true} /></Box>;
|
||||
$[11] = cursorOffset;
|
||||
$[12] = handleSubmit;
|
||||
$[13] = systemPrompt;
|
||||
$[14] = t7;
|
||||
} else {
|
||||
t7 = $[14];
|
||||
}
|
||||
let t8;
|
||||
if ($[15] !== error) {
|
||||
t8 = error && <Box marginTop={1}><Text color="error">{error}</Text></Box>;
|
||||
$[15] = error;
|
||||
$[16] = t8;
|
||||
} else {
|
||||
t8 = $[16];
|
||||
}
|
||||
let t9;
|
||||
if ($[17] !== t7 || $[18] !== t8) {
|
||||
t9 = <WizardDialogLayout subtitle="System prompt" footerText={t4}><Box flexDirection="column">{t5}{t6}{t7}{t8}</Box></WizardDialogLayout>;
|
||||
$[17] = t7;
|
||||
$[18] = t8;
|
||||
$[19] = t9;
|
||||
} else {
|
||||
t9 = $[19];
|
||||
}
|
||||
return t9;
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
<Text>Enter the system prompt for your agent:</Text>
|
||||
<Text dimColor>Be comprehensive for best results</Text>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<TextInput
|
||||
value={systemPrompt}
|
||||
onChange={setSystemPrompt}
|
||||
onSubmit={handleSubmit}
|
||||
placeholder="You are a helpful code reviewer who..."
|
||||
columns={80}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
focus
|
||||
showCursor
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="error">{error}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</WizardDialogLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,60 +1,52 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { type ReactNode } from 'react';
|
||||
import type { Tools } from '../../../../Tool.js';
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../../design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
||||
import { useWizard } from '../../../wizard/index.js';
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
||||
import { ToolSelector } from '../../ToolSelector.js';
|
||||
import type { AgentWizardData } from '../types.js';
|
||||
import React, { type ReactNode } from 'react'
|
||||
import type { Tools } from '../../../../Tool.js'
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||
import { Byline } from '../../../design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||
import { useWizard } from '../../../wizard/index.js'
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||
import { ToolSelector } from '../../ToolSelector.js'
|
||||
import type { AgentWizardData } from '../types.js'
|
||||
|
||||
type Props = {
|
||||
tools: Tools;
|
||||
};
|
||||
export function ToolsStep(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
tools
|
||||
} = t0;
|
||||
const {
|
||||
goNext,
|
||||
goBack,
|
||||
updateWizardData,
|
||||
wizardData
|
||||
} = useWizard();
|
||||
let t1;
|
||||
if ($[0] !== goNext || $[1] !== updateWizardData) {
|
||||
t1 = selectedTools => {
|
||||
updateWizardData({
|
||||
selectedTools
|
||||
});
|
||||
goNext();
|
||||
};
|
||||
$[0] = goNext;
|
||||
$[1] = updateWizardData;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const handleComplete = t1;
|
||||
const initialTools = wizardData.selectedTools;
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Byline><KeyboardShortcutHint shortcut="Enter" action="toggle selection" /><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] !== goBack || $[5] !== handleComplete || $[6] !== initialTools || $[7] !== tools) {
|
||||
t3 = <WizardDialogLayout subtitle="Select tools" footerText={t2}><ToolSelector tools={tools} initialTools={initialTools} onComplete={handleComplete} onCancel={goBack} /></WizardDialogLayout>;
|
||||
$[4] = goBack;
|
||||
$[5] = handleComplete;
|
||||
$[6] = initialTools;
|
||||
$[7] = tools;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
return t3;
|
||||
tools: Tools
|
||||
}
|
||||
|
||||
export function ToolsStep({ tools }: Props): ReactNode {
|
||||
const { goNext, goBack, updateWizardData, wizardData } =
|
||||
useWizard<AgentWizardData>()
|
||||
|
||||
const handleComplete = (selectedTools: string[] | undefined): void => {
|
||||
updateWizardData({ selectedTools })
|
||||
goNext()
|
||||
}
|
||||
|
||||
// Pass through undefined to preserve "all tools" semantic
|
||||
// ToolSelector will expand it internally for display purposes
|
||||
const initialTools = wizardData.selectedTools
|
||||
|
||||
return (
|
||||
<WizardDialogLayout
|
||||
subtitle="Select tools"
|
||||
footerText={
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="Enter" action="toggle selection" />
|
||||
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="go back"
|
||||
/>
|
||||
</Byline>
|
||||
}
|
||||
>
|
||||
<ToolSelector
|
||||
tools={tools}
|
||||
initialTools={initialTools}
|
||||
onComplete={handleComplete}
|
||||
onCancel={goBack}
|
||||
/>
|
||||
</WizardDialogLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,102 +1,83 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { type ReactNode, useState } from 'react';
|
||||
import { Box, Text } from '../../../../ink.js';
|
||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
|
||||
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js';
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../../design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
||||
import TextInput from '../../../TextInput.js';
|
||||
import { useWizard } from '../../../wizard/index.js';
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
||||
import { validateAgentType } from '../../validateAgent.js';
|
||||
import type { AgentWizardData } from '../types.js';
|
||||
import React, { type ReactNode, useState } from 'react'
|
||||
import { Box, Text } from '../../../../ink.js'
|
||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
|
||||
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'
|
||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||
import { Byline } from '../../../design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||
import TextInput from '../../../TextInput.js'
|
||||
import { useWizard } from '../../../wizard/index.js'
|
||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||
import { validateAgentType } from '../../validateAgent.js'
|
||||
import type { AgentWizardData } from '../types.js'
|
||||
|
||||
type Props = {
|
||||
existingAgents: AgentDefinition[];
|
||||
};
|
||||
export function TypeStep(_props) {
|
||||
const $ = _c(15);
|
||||
const {
|
||||
goNext,
|
||||
goBack,
|
||||
updateWizardData,
|
||||
wizardData
|
||||
} = useWizard();
|
||||
const [agentType, setAgentType] = useState(wizardData.agentType || "");
|
||||
const [error, setError] = useState(null);
|
||||
const [cursorOffset, setCursorOffset] = useState(agentType.length);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = {
|
||||
context: "Settings"
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
useKeybinding("confirm:no", goBack, t0);
|
||||
let t1;
|
||||
if ($[1] !== goNext || $[2] !== updateWizardData) {
|
||||
t1 = value => {
|
||||
const trimmedValue = value.trim();
|
||||
const validationError = validateAgentType(trimmedValue);
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
updateWizardData({
|
||||
agentType: trimmedValue
|
||||
});
|
||||
goNext();
|
||||
};
|
||||
$[1] = goNext;
|
||||
$[2] = updateWizardData;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const handleSubmit = t1;
|
||||
let t2;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Byline><KeyboardShortcutHint shortcut="Type" action="enter text" /><KeyboardShortcutHint shortcut="Enter" action="continue" /><ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="go back" /></Byline>;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Text>Enter a unique identifier for your agent:</Text>;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== agentType || $[7] !== cursorOffset || $[8] !== handleSubmit) {
|
||||
t4 = <Box marginTop={1}><TextInput value={agentType} onChange={setAgentType} onSubmit={handleSubmit} placeholder="e.g., test-runner, tech-lead, etc" columns={60} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus={true} showCursor={true} /></Box>;
|
||||
$[6] = agentType;
|
||||
$[7] = cursorOffset;
|
||||
$[8] = handleSubmit;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== error) {
|
||||
t5 = error && <Box marginTop={1}><Text color="error">{error}</Text></Box>;
|
||||
$[10] = error;
|
||||
$[11] = t5;
|
||||
} else {
|
||||
t5 = $[11];
|
||||
}
|
||||
let t6;
|
||||
if ($[12] !== t4 || $[13] !== t5) {
|
||||
t6 = <WizardDialogLayout subtitle="Agent type (identifier)" footerText={t2}><Box flexDirection="column">{t3}{t4}{t5}</Box></WizardDialogLayout>;
|
||||
$[12] = t4;
|
||||
$[13] = t5;
|
||||
$[14] = t6;
|
||||
} else {
|
||||
t6 = $[14];
|
||||
}
|
||||
return t6;
|
||||
existingAgents: AgentDefinition[]
|
||||
}
|
||||
|
||||
export function TypeStep(_props: Props): ReactNode {
|
||||
const { goNext, goBack, updateWizardData, wizardData } =
|
||||
useWizard<AgentWizardData>()
|
||||
const [agentType, setAgentType] = useState(wizardData.agentType || '')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [cursorOffset, setCursorOffset] = useState(agentType.length)
|
||||
|
||||
// Handle escape key - Go back to MethodStep
|
||||
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
|
||||
useKeybinding('confirm:no', goBack, { context: 'Settings' })
|
||||
|
||||
const handleSubmit = (value: string): void => {
|
||||
const trimmedValue = value.trim()
|
||||
const validationError = validateAgentType(trimmedValue)
|
||||
|
||||
if (validationError) {
|
||||
setError(validationError)
|
||||
return
|
||||
}
|
||||
|
||||
setError(null)
|
||||
updateWizardData({ agentType: trimmedValue })
|
||||
goNext()
|
||||
}
|
||||
|
||||
return (
|
||||
<WizardDialogLayout
|
||||
subtitle="Agent type (identifier)"
|
||||
footerText={
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="Type" action="enter text" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="continue" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Settings"
|
||||
fallback="Esc"
|
||||
description="go back"
|
||||
/>
|
||||
</Byline>
|
||||
}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
<Text>Enter a unique identifier for your agent:</Text>
|
||||
<Box marginTop={1}>
|
||||
<TextInput
|
||||
value={agentType}
|
||||
onChange={setAgentType}
|
||||
onSubmit={handleSubmit}
|
||||
placeholder="e.g., test-runner, tech-lead, etc"
|
||||
columns={60}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
focus
|
||||
showCursor
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="error">{error}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</WizardDialogLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { Children, isValidElement } from 'react';
|
||||
import { Text } from '../../ink.js';
|
||||
import React, { Children, isValidElement } from 'react'
|
||||
import { Text } from '../../ink.js'
|
||||
|
||||
type Props = {
|
||||
/** The items to join with a middot separator */
|
||||
children: React.ReactNode;
|
||||
};
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins children with a middot separator (" · ") for inline metadata display.
|
||||
@@ -34,43 +34,24 @@ type Props = {
|
||||
* </Text>
|
||||
*
|
||||
*/
|
||||
export function Byline(t0) {
|
||||
const $ = _c(5);
|
||||
const {
|
||||
children
|
||||
} = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== children) {
|
||||
t2 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const validChildren = Children.toArray(children);
|
||||
if (validChildren.length === 0) {
|
||||
t2 = null;
|
||||
break bb0;
|
||||
}
|
||||
t1 = validChildren.map(_temp);
|
||||
}
|
||||
$[0] = children;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
export function Byline({ children }: Props): React.ReactNode {
|
||||
// Children.toArray already filters out null, undefined, and booleans
|
||||
const validChildren = Children.toArray(children)
|
||||
|
||||
if (validChildren.length === 0) {
|
||||
return null
|
||||
}
|
||||
if (t2 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t2;
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== t1) {
|
||||
t3 = <>{t1}</>;
|
||||
$[3] = t1;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
function _temp(child, index) {
|
||||
return <React.Fragment key={isValidElement(child) ? child.key ?? index : index}>{index > 0 && <Text dimColor={true}> · </Text>}{child}</React.Fragment>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{validChildren.map((child, index) => (
|
||||
<React.Fragment
|
||||
key={isValidElement(child) ? (child.key ?? index) : index}
|
||||
>
|
||||
{index > 0 && <Text dimColor> · </Text>}
|
||||
{child}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { type ExitState, useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
||||
import { Byline } from './Byline.js';
|
||||
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js';
|
||||
import { Pane } from './Pane.js';
|
||||
import React from 'react'
|
||||
import {
|
||||
type ExitState,
|
||||
useExitOnCtrlCDWithKeybindings,
|
||||
} from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import type { Theme } from '../../utils/theme.js'
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||
import { Byline } from './Byline.js'
|
||||
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'
|
||||
import { Pane } from './Pane.js'
|
||||
|
||||
type DialogProps = {
|
||||
title: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
onCancel: () => void;
|
||||
color?: keyof Theme;
|
||||
hideInputGuide?: boolean;
|
||||
hideBorder?: boolean;
|
||||
title: React.ReactNode
|
||||
subtitle?: React.ReactNode
|
||||
children: React.ReactNode
|
||||
onCancel: () => void
|
||||
color?: keyof Theme
|
||||
hideInputGuide?: boolean
|
||||
hideBorder?: boolean
|
||||
/** Custom input guide content. Receives exitState for Ctrl+C/D pending display. */
|
||||
inputGuide?: (exitState: ExitState) => React.ReactNode;
|
||||
inputGuide?: (exitState: ExitState) => React.ReactNode
|
||||
/**
|
||||
* Controls whether Dialog's built-in confirm:no (Esc/n) and app:exit/interrupt
|
||||
* (Ctrl-C/D) keybindings are active. Set to `false` while an embedded text
|
||||
@@ -25,113 +28,73 @@ type DialogProps = {
|
||||
* consumed by Dialog. TextInput has its own ctrl+c/d handlers (cancel on
|
||||
* press, delete-forward on ctrl+d with text). Defaults to `true`.
|
||||
*/
|
||||
isCancelActive?: boolean;
|
||||
};
|
||||
export function Dialog(t0) {
|
||||
const $ = _c(27);
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
onCancel,
|
||||
color: t1,
|
||||
hideInputGuide,
|
||||
hideBorder,
|
||||
inputGuide,
|
||||
isCancelActive: t2
|
||||
} = t0;
|
||||
const color = t1 === undefined ? "permission" : t1;
|
||||
const isCancelActive = t2 === undefined ? true : t2;
|
||||
const exitState = useExitOnCtrlCDWithKeybindings(undefined, undefined, isCancelActive);
|
||||
let t3;
|
||||
if ($[0] !== isCancelActive) {
|
||||
t3 = {
|
||||
context: "Confirmation",
|
||||
isActive: isCancelActive
|
||||
};
|
||||
$[0] = isCancelActive;
|
||||
$[1] = t3;
|
||||
} else {
|
||||
t3 = $[1];
|
||||
}
|
||||
useKeybinding("confirm:no", onCancel, t3);
|
||||
let t4;
|
||||
if ($[2] !== exitState.keyName || $[3] !== exitState.pending) {
|
||||
t4 = exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline><KeyboardShortcutHint shortcut="Enter" action="confirm" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /></Byline>;
|
||||
$[2] = exitState.keyName;
|
||||
$[3] = exitState.pending;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
const defaultInputGuide = t4;
|
||||
let t5;
|
||||
if ($[5] !== color || $[6] !== title) {
|
||||
t5 = <Text bold={true} color={color}>{title}</Text>;
|
||||
$[5] = color;
|
||||
$[6] = title;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] !== subtitle) {
|
||||
t6 = subtitle && <Text dimColor={true}>{subtitle}</Text>;
|
||||
$[8] = subtitle;
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
let t7;
|
||||
if ($[10] !== t5 || $[11] !== t6) {
|
||||
t7 = <Box flexDirection="column">{t5}{t6}</Box>;
|
||||
$[10] = t5;
|
||||
$[11] = t6;
|
||||
$[12] = t7;
|
||||
} else {
|
||||
t7 = $[12];
|
||||
}
|
||||
let t8;
|
||||
if ($[13] !== children || $[14] !== t7) {
|
||||
t8 = <Box flexDirection="column" gap={1}>{t7}{children}</Box>;
|
||||
$[13] = children;
|
||||
$[14] = t7;
|
||||
$[15] = t8;
|
||||
} else {
|
||||
t8 = $[15];
|
||||
}
|
||||
let t9;
|
||||
if ($[16] !== defaultInputGuide || $[17] !== exitState || $[18] !== hideInputGuide || $[19] !== inputGuide) {
|
||||
t9 = !hideInputGuide && <Box marginTop={1}><Text dimColor={true} italic={true}>{inputGuide ? inputGuide(exitState) : defaultInputGuide}</Text></Box>;
|
||||
$[16] = defaultInputGuide;
|
||||
$[17] = exitState;
|
||||
$[18] = hideInputGuide;
|
||||
$[19] = inputGuide;
|
||||
$[20] = t9;
|
||||
} else {
|
||||
t9 = $[20];
|
||||
}
|
||||
let t10;
|
||||
if ($[21] !== t8 || $[22] !== t9) {
|
||||
t10 = <>{t8}{t9}</>;
|
||||
$[21] = t8;
|
||||
$[22] = t9;
|
||||
$[23] = t10;
|
||||
} else {
|
||||
t10 = $[23];
|
||||
}
|
||||
const content = t10;
|
||||
if (hideBorder) {
|
||||
return content;
|
||||
}
|
||||
let t11;
|
||||
if ($[24] !== color || $[25] !== content) {
|
||||
t11 = <Pane color={color}>{content}</Pane>;
|
||||
$[24] = color;
|
||||
$[25] = content;
|
||||
$[26] = t11;
|
||||
} else {
|
||||
t11 = $[26];
|
||||
}
|
||||
return t11;
|
||||
isCancelActive?: boolean
|
||||
}
|
||||
|
||||
export function Dialog({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
onCancel,
|
||||
color = 'permission',
|
||||
hideInputGuide,
|
||||
hideBorder,
|
||||
inputGuide,
|
||||
isCancelActive = true,
|
||||
}: DialogProps): React.ReactNode {
|
||||
const exitState = useExitOnCtrlCDWithKeybindings(
|
||||
undefined,
|
||||
undefined,
|
||||
isCancelActive,
|
||||
)
|
||||
|
||||
// Use configurable keybinding for ESC to cancel.
|
||||
// isCancelActive lets consumers (e.g. ElicitationDialog) disable this while
|
||||
// an embedded TextInput is focused, so that keys like 'n' reach the field
|
||||
// instead of being consumed here.
|
||||
useKeybinding('confirm:no', onCancel, {
|
||||
context: 'Confirmation',
|
||||
isActive: isCancelActive,
|
||||
})
|
||||
|
||||
const defaultInputGuide = exitState.pending ? (
|
||||
<Text>Press {exitState.keyName} again to exit</Text>
|
||||
) : (
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="cancel"
|
||||
/>
|
||||
</Byline>
|
||||
)
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={color}>
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle && <Text dimColor>{subtitle}</Text>}
|
||||
</Box>
|
||||
{children}
|
||||
</Box>
|
||||
{!hideInputGuide && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor italic>
|
||||
{inputGuide ? inputGuide(exitState) : defaultInputGuide}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
if (hideBorder) {
|
||||
return content
|
||||
}
|
||||
|
||||
return <Pane color={color}>{content}</Pane>
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import { Ansi, Text } from '../../ink.js';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
import React from 'react'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { Ansi, Text } from '../../ink.js'
|
||||
import type { Theme } from '../../utils/theme.js'
|
||||
|
||||
type DividerProps = {
|
||||
/**
|
||||
* Width of the divider in characters.
|
||||
* Defaults to terminal width.
|
||||
*/
|
||||
width?: number;
|
||||
width?: number
|
||||
|
||||
/**
|
||||
* Theme color for the divider.
|
||||
* If not provided, dimColor is used.
|
||||
*/
|
||||
color?: keyof Theme;
|
||||
color?: keyof Theme
|
||||
|
||||
/**
|
||||
* Character to use for the divider line.
|
||||
* @default '─'
|
||||
*/
|
||||
char?: string;
|
||||
char?: string
|
||||
|
||||
/**
|
||||
* Padding to subtract from the width (e.g., for indentation).
|
||||
* @default 0
|
||||
*/
|
||||
padding?: number;
|
||||
padding?: number
|
||||
|
||||
/**
|
||||
* Title shown in the middle of the divider.
|
||||
@@ -37,8 +37,8 @@ type DividerProps = {
|
||||
* // ─────────── Title ───────────
|
||||
* <Divider title="Title" />
|
||||
*/
|
||||
title?: string;
|
||||
};
|
||||
title?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A horizontal divider line.
|
||||
@@ -63,86 +63,35 @@ type DividerProps = {
|
||||
* // With centered title
|
||||
* <Divider title="3 new messages" />
|
||||
*/
|
||||
export function Divider(t0) {
|
||||
const $ = _c(21);
|
||||
const {
|
||||
width,
|
||||
color,
|
||||
char: t1,
|
||||
padding: t2,
|
||||
title
|
||||
} = t0;
|
||||
const char = t1 === undefined ? "\u2500" : t1;
|
||||
const padding = t2 === undefined ? 0 : t2;
|
||||
const {
|
||||
columns: terminalWidth
|
||||
} = useTerminalSize();
|
||||
const effectiveWidth = Math.max(0, (width ?? terminalWidth) - padding);
|
||||
export function Divider({
|
||||
width,
|
||||
color,
|
||||
char = '─',
|
||||
padding = 0,
|
||||
title,
|
||||
}: DividerProps): React.ReactNode {
|
||||
const { columns: terminalWidth } = useTerminalSize()
|
||||
const effectiveWidth = Math.max(0, (width ?? terminalWidth) - padding)
|
||||
|
||||
if (title) {
|
||||
const titleWidth = stringWidth(title) + 2;
|
||||
const sideWidth = Math.max(0, effectiveWidth - titleWidth);
|
||||
const leftWidth = Math.floor(sideWidth / 2);
|
||||
const rightWidth = sideWidth - leftWidth;
|
||||
const t3 = !color;
|
||||
let t4;
|
||||
if ($[0] !== char || $[1] !== leftWidth) {
|
||||
t4 = char.repeat(leftWidth);
|
||||
$[0] = char;
|
||||
$[1] = leftWidth;
|
||||
$[2] = t4;
|
||||
} else {
|
||||
t4 = $[2];
|
||||
}
|
||||
let t5;
|
||||
if ($[3] !== title) {
|
||||
t5 = <Text dimColor={true}><Ansi>{title}</Ansi></Text>;
|
||||
$[3] = title;
|
||||
$[4] = t5;
|
||||
} else {
|
||||
t5 = $[4];
|
||||
}
|
||||
let t6;
|
||||
if ($[5] !== char || $[6] !== rightWidth) {
|
||||
t6 = char.repeat(rightWidth);
|
||||
$[5] = char;
|
||||
$[6] = rightWidth;
|
||||
$[7] = t6;
|
||||
} else {
|
||||
t6 = $[7];
|
||||
}
|
||||
let t7;
|
||||
if ($[8] !== color || $[9] !== t3 || $[10] !== t4 || $[11] !== t5 || $[12] !== t6) {
|
||||
t7 = <Text color={color} dimColor={t3}>{t4}{" "}{t5}{" "}{t6}</Text>;
|
||||
$[8] = color;
|
||||
$[9] = t3;
|
||||
$[10] = t4;
|
||||
$[11] = t5;
|
||||
$[12] = t6;
|
||||
$[13] = t7;
|
||||
} else {
|
||||
t7 = $[13];
|
||||
}
|
||||
return t7;
|
||||
const titleWidth = stringWidth(title) + 2 // +2 for spaces around title
|
||||
const sideWidth = Math.max(0, effectiveWidth - titleWidth)
|
||||
const leftWidth = Math.floor(sideWidth / 2)
|
||||
const rightWidth = sideWidth - leftWidth
|
||||
return (
|
||||
<Text color={color} dimColor={!color}>
|
||||
{char.repeat(leftWidth)}{' '}
|
||||
<Text dimColor>
|
||||
<Ansi>{title}</Ansi>
|
||||
</Text>{' '}
|
||||
{char.repeat(rightWidth)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
const t3 = !color;
|
||||
let t4;
|
||||
if ($[14] !== char || $[15] !== effectiveWidth) {
|
||||
t4 = char.repeat(effectiveWidth);
|
||||
$[14] = char;
|
||||
$[15] = effectiveWidth;
|
||||
$[16] = t4;
|
||||
} else {
|
||||
t4 = $[16];
|
||||
}
|
||||
let t5;
|
||||
if ($[17] !== color || $[18] !== t3 || $[19] !== t4) {
|
||||
t5 = <Text color={color} dimColor={t3}>{t4}</Text>;
|
||||
$[17] = color;
|
||||
$[18] = t3;
|
||||
$[19] = t4;
|
||||
$[20] = t5;
|
||||
} else {
|
||||
t5 = $[20];
|
||||
}
|
||||
return t5;
|
||||
|
||||
return (
|
||||
<Text color={color} dimColor={!color}>
|
||||
{char.repeat(effectiveWidth)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,70 +1,73 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchInput } from '../../hooks/useSearchInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { clamp } from '../../ink/layout/geometry.js';
|
||||
import { Box, Text, useTerminalFocus } from '../../ink.js';
|
||||
import { SearchBox } from '../SearchBox.js';
|
||||
import { Byline } from './Byline.js';
|
||||
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js';
|
||||
import { ListItem } from './ListItem.js';
|
||||
import { Pane } from './Pane.js';
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSearchInput } from '../../hooks/useSearchInput.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { clamp } from '../../ink/layout/geometry.js'
|
||||
import { Box, Text, useTerminalFocus } from '../../ink.js'
|
||||
import { SearchBox } from '../SearchBox.js'
|
||||
import { Byline } from './Byline.js'
|
||||
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'
|
||||
import { ListItem } from './ListItem.js'
|
||||
import { Pane } from './Pane.js'
|
||||
|
||||
type PickerAction<T> = {
|
||||
/** Hint label shown in the byline, e.g. "mention" → "Tab to mention". */
|
||||
action: string;
|
||||
handler: (item: T) => void;
|
||||
};
|
||||
action: string
|
||||
handler: (item: T) => void
|
||||
}
|
||||
|
||||
type Props<T> = {
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
initialQuery?: string;
|
||||
items: readonly T[];
|
||||
getKey: (item: T) => string;
|
||||
title: string
|
||||
placeholder?: string
|
||||
initialQuery?: string
|
||||
items: readonly T[]
|
||||
getKey: (item: T) => string
|
||||
/** Keep to one line — preview handles overflow. */
|
||||
renderItem: (item: T, isFocused: boolean) => React.ReactNode;
|
||||
renderPreview?: (item: T) => React.ReactNode;
|
||||
renderItem: (item: T, isFocused: boolean) => React.ReactNode
|
||||
renderPreview?: (item: T) => React.ReactNode
|
||||
/** 'right' keeps hints stable (no bounce), but needs width. */
|
||||
previewPosition?: 'bottom' | 'right';
|
||||
visibleCount?: number;
|
||||
previewPosition?: 'bottom' | 'right'
|
||||
visibleCount?: number
|
||||
/**
|
||||
* 'up' puts items[0] at the bottom next to the input (atuin-style). Arrows
|
||||
* always match screen direction — ↑ walks visually up regardless.
|
||||
*/
|
||||
direction?: 'down' | 'up';
|
||||
direction?: 'down' | 'up'
|
||||
/** Caller owns filtering: re-filter on each call and pass new items. */
|
||||
onQueryChange: (query: string) => void;
|
||||
onQueryChange: (query: string) => void
|
||||
/** Enter key. Primary action. */
|
||||
onSelect: (item: T) => void;
|
||||
onSelect: (item: T) => void
|
||||
/**
|
||||
* Tab key. If provided, Tab no longer aliases Enter — it gets its own
|
||||
* handler and hint. Shift+Tab falls through to this if onShiftTab is unset.
|
||||
*/
|
||||
onTab?: PickerAction<T>;
|
||||
onTab?: PickerAction<T>
|
||||
/** Shift+Tab key. Gets its own hint. */
|
||||
onShiftTab?: PickerAction<T>;
|
||||
onShiftTab?: PickerAction<T>
|
||||
/**
|
||||
* Fires when the focused item changes (via arrows or when items reset).
|
||||
* Useful for async preview loading — keeps I/O out of renderPreview.
|
||||
*/
|
||||
onFocus?: (item: T | undefined) => void;
|
||||
onCancel: () => void;
|
||||
onFocus?: (item: T | undefined) => void
|
||||
onCancel: () => void
|
||||
/** Shown when items is empty. Caller bakes loading/searching state into this. */
|
||||
emptyMessage?: string | ((query: string) => string);
|
||||
emptyMessage?: string | ((query: string) => string)
|
||||
/**
|
||||
* Status line below the list, e.g. "500+ matches" or "42 matches…".
|
||||
* Caller decides when to show it — pass undefined to hide.
|
||||
*/
|
||||
matchLabel?: string;
|
||||
selectAction?: string;
|
||||
extraHints?: React.ReactNode;
|
||||
};
|
||||
const DEFAULT_VISIBLE = 8;
|
||||
matchLabel?: string
|
||||
selectAction?: string
|
||||
extraHints?: React.ReactNode
|
||||
}
|
||||
|
||||
const DEFAULT_VISIBLE = 8
|
||||
// Pane (paddingTop + Divider) + title + 3 gaps + SearchBox (rounded border = 3
|
||||
// rows) + hints. matchLabel adds +1 when present, accounted for separately.
|
||||
const CHROME_ROWS = 10;
|
||||
const MIN_VISIBLE = 2;
|
||||
const CHROME_ROWS = 10
|
||||
const MIN_VISIBLE = 2
|
||||
|
||||
export function FuzzyPicker<T>({
|
||||
title,
|
||||
placeholder = 'Type to search…',
|
||||
@@ -85,117 +88,168 @@ export function FuzzyPicker<T>({
|
||||
emptyMessage = 'No results',
|
||||
matchLabel,
|
||||
selectAction = 'select',
|
||||
extraHints
|
||||
extraHints,
|
||||
}: Props<T>): React.ReactNode {
|
||||
const isTerminalFocused = useTerminalFocus();
|
||||
const {
|
||||
rows,
|
||||
columns
|
||||
} = useTerminalSize();
|
||||
const [focusedIndex, setFocusedIndex] = useState(0);
|
||||
const isTerminalFocused = useTerminalFocus()
|
||||
const { rows, columns } = useTerminalSize()
|
||||
const [focusedIndex, setFocusedIndex] = useState(0)
|
||||
|
||||
// Cap visibleCount so the picker never exceeds the terminal height. When it
|
||||
// overflows, each re-render (arrow key, ctrl+p) mis-positions the cursor-up
|
||||
// by the overflow amount and a previously-drawn line flashes blank.
|
||||
const visibleCount = Math.max(MIN_VISIBLE, Math.min(requestedVisible, rows - CHROME_ROWS - (matchLabel ? 1 : 0)));
|
||||
const visibleCount = Math.max(
|
||||
MIN_VISIBLE,
|
||||
Math.min(requestedVisible, rows - CHROME_ROWS - (matchLabel ? 1 : 0)),
|
||||
)
|
||||
|
||||
// Full hint row with onTab+onShiftTab is ~100 chars and wraps inconsistently
|
||||
// below that. Compact mode drops shift+tab and shortens labels.
|
||||
const compact = columns < 120;
|
||||
const compact = columns < 120
|
||||
|
||||
const step = (delta: 1 | -1) => {
|
||||
setFocusedIndex(i => clamp(i + delta, 0, items.length - 1));
|
||||
};
|
||||
setFocusedIndex(i => clamp(i + delta, 0, items.length - 1))
|
||||
}
|
||||
|
||||
// onKeyDown fires after useSearchInput's useInput, so onExit must be a
|
||||
// no-op — return/downArrow are handled by handleKeyDown below. onCancel
|
||||
// still covers escape/ctrl+c/ctrl+d. Backspace-on-empty is disabled so
|
||||
// a held backspace doesn't eject the user from the dialog.
|
||||
const {
|
||||
query,
|
||||
cursorOffset
|
||||
} = useSearchInput({
|
||||
const { query, cursorOffset } = useSearchInput({
|
||||
isActive: true,
|
||||
onExit: () => {},
|
||||
onCancel,
|
||||
initialQuery,
|
||||
backspaceExitsOnEmpty: false
|
||||
});
|
||||
backspaceExitsOnEmpty: false,
|
||||
})
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'up' || e.ctrl && e.key === 'p') {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
step(direction === 'up' ? 1 : -1);
|
||||
return;
|
||||
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
step(direction === 'up' ? 1 : -1)
|
||||
return
|
||||
}
|
||||
if (e.key === 'down' || e.ctrl && e.key === 'n') {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
step(direction === 'up' ? -1 : 1);
|
||||
return;
|
||||
if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
step(direction === 'up' ? -1 : 1)
|
||||
return
|
||||
}
|
||||
if (e.key === 'return') {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
const selected = items[focusedIndex];
|
||||
if (selected) onSelect(selected);
|
||||
return;
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
const selected = items[focusedIndex]
|
||||
if (selected) onSelect(selected)
|
||||
return
|
||||
}
|
||||
if (e.key === 'tab') {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
const selected = items[focusedIndex];
|
||||
if (!selected) return;
|
||||
const tabAction = e.shift ? onShiftTab ?? onTab : onTab;
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
const selected = items[focusedIndex]
|
||||
if (!selected) return
|
||||
const tabAction = e.shift ? (onShiftTab ?? onTab) : onTab
|
||||
if (tabAction) {
|
||||
tabAction.handler(selected);
|
||||
tabAction.handler(selected)
|
||||
} else {
|
||||
onSelect(selected);
|
||||
onSelect(selected)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
onQueryChange(query);
|
||||
setFocusedIndex(0);
|
||||
onQueryChange(query)
|
||||
setFocusedIndex(0)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [query]);
|
||||
}, [query])
|
||||
|
||||
useEffect(() => {
|
||||
setFocusedIndex(i => clamp(i, 0, items.length - 1));
|
||||
}, [items.length]);
|
||||
const focused = items[focusedIndex];
|
||||
setFocusedIndex(i => clamp(i, 0, items.length - 1))
|
||||
}, [items.length])
|
||||
|
||||
const focused = items[focusedIndex]
|
||||
useEffect(() => {
|
||||
onFocus?.(focused);
|
||||
onFocus?.(focused)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [focused]);
|
||||
const windowStart = clamp(focusedIndex - visibleCount + 1, 0, items.length - visibleCount);
|
||||
const visible = items.slice(windowStart, windowStart + visibleCount);
|
||||
const emptyText = typeof emptyMessage === 'function' ? emptyMessage(query) : emptyMessage;
|
||||
const searchBox = <SearchBox query={query} cursorOffset={cursorOffset} placeholder={placeholder} isFocused isTerminalFocused={isTerminalFocused} />;
|
||||
const listBlock = <List visible={visible} windowStart={windowStart} visibleCount={visibleCount} total={items.length} focusedIndex={focusedIndex} direction={direction} getKey={getKey} renderItem={renderItem} emptyText={emptyText} />;
|
||||
const preview = renderPreview && focused ? <Box flexDirection="column" flexGrow={1}>
|
||||
}, [focused])
|
||||
|
||||
const windowStart = clamp(
|
||||
focusedIndex - visibleCount + 1,
|
||||
0,
|
||||
items.length - visibleCount,
|
||||
)
|
||||
const visible = items.slice(windowStart, windowStart + visibleCount)
|
||||
|
||||
const emptyText =
|
||||
typeof emptyMessage === 'function' ? emptyMessage(query) : emptyMessage
|
||||
|
||||
const searchBox = (
|
||||
<SearchBox
|
||||
query={query}
|
||||
cursorOffset={cursorOffset}
|
||||
placeholder={placeholder}
|
||||
isFocused
|
||||
isTerminalFocused={isTerminalFocused}
|
||||
/>
|
||||
)
|
||||
|
||||
const listBlock = (
|
||||
<List
|
||||
visible={visible}
|
||||
windowStart={windowStart}
|
||||
visibleCount={visibleCount}
|
||||
total={items.length}
|
||||
focusedIndex={focusedIndex}
|
||||
direction={direction}
|
||||
getKey={getKey}
|
||||
renderItem={renderItem}
|
||||
emptyText={emptyText}
|
||||
/>
|
||||
)
|
||||
|
||||
const preview =
|
||||
renderPreview && focused ? (
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
{renderPreview(focused)}
|
||||
</Box> : null;
|
||||
</Box>
|
||||
) : null
|
||||
|
||||
// Structure must not depend on preview truthiness — when focused goes
|
||||
// undefined (e.g. delete clears matches), switching row→fragment would
|
||||
// change both layout AND gap count, bouncing the searchBox below.
|
||||
const listGroup = renderPreview && previewPosition === 'right' ? <Box flexDirection="row" gap={2} height={visibleCount + (matchLabel ? 1 : 0)}>
|
||||
const listGroup =
|
||||
renderPreview && previewPosition === 'right' ? (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
gap={2}
|
||||
height={visibleCount + (matchLabel ? 1 : 0)}
|
||||
>
|
||||
<Box flexDirection="column" flexShrink={0}>
|
||||
{listBlock}
|
||||
{matchLabel && <Text dimColor>{matchLabel}</Text>}
|
||||
</Box>
|
||||
{preview ?? <Box flexGrow={1} />}
|
||||
</Box> :
|
||||
// Box (not fragment) so the outer gap={1} doesn't insert a blank line
|
||||
// between list/matchLabel/preview — that read as extra space above the
|
||||
// prompt in direction='up'.
|
||||
<Box flexDirection="column">
|
||||
</Box>
|
||||
) : (
|
||||
// Box (not fragment) so the outer gap={1} doesn't insert a blank line
|
||||
// between list/matchLabel/preview — that read as extra space above the
|
||||
// prompt in direction='up'.
|
||||
<Box flexDirection="column">
|
||||
{listBlock}
|
||||
{matchLabel && <Text dimColor>{matchLabel}</Text>}
|
||||
{preview}
|
||||
</Box>;
|
||||
const inputAbove = direction !== 'up';
|
||||
return <Pane color="permission">
|
||||
<Box flexDirection="column" gap={1} tabIndex={0} autoFocus onKeyDown={handleKeyDown}>
|
||||
</Box>
|
||||
)
|
||||
|
||||
const inputAbove = direction !== 'up'
|
||||
return (
|
||||
<Pane color="permission">
|
||||
<Box
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Text bold color="permission">
|
||||
{title}
|
||||
</Text>
|
||||
@@ -204,108 +258,93 @@ export function FuzzyPicker<T>({
|
||||
{!inputAbove && searchBox}
|
||||
<Text dimColor>
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="↑/↓" action={compact ? 'nav' : 'navigate'} />
|
||||
<KeyboardShortcutHint shortcut="Enter" action={compact ? firstWord(selectAction) : selectAction} />
|
||||
{onTab && <KeyboardShortcutHint shortcut="Tab" action={onTab.action} />}
|
||||
{onShiftTab && !compact && <KeyboardShortcutHint shortcut="shift+tab" action={onShiftTab.action} />}
|
||||
<KeyboardShortcutHint
|
||||
shortcut="↑/↓"
|
||||
action={compact ? 'nav' : 'navigate'}
|
||||
/>
|
||||
<KeyboardShortcutHint
|
||||
shortcut="Enter"
|
||||
action={compact ? firstWord(selectAction) : selectAction}
|
||||
/>
|
||||
{onTab && (
|
||||
<KeyboardShortcutHint shortcut="Tab" action={onTab.action} />
|
||||
)}
|
||||
{onShiftTab && !compact && (
|
||||
<KeyboardShortcutHint
|
||||
shortcut="shift+tab"
|
||||
action={onShiftTab.action}
|
||||
/>
|
||||
)}
|
||||
<KeyboardShortcutHint shortcut="Esc" action="cancel" />
|
||||
{extraHints}
|
||||
</Byline>
|
||||
</Text>
|
||||
</Box>
|
||||
</Pane>;
|
||||
</Pane>
|
||||
)
|
||||
}
|
||||
type ListProps<T> = Pick<Props<T>, 'visibleCount' | 'direction' | 'getKey' | 'renderItem'> & {
|
||||
visible: readonly T[];
|
||||
windowStart: number;
|
||||
total: number;
|
||||
focusedIndex: number;
|
||||
emptyText: string;
|
||||
};
|
||||
function List(t0) {
|
||||
const $ = _c(27);
|
||||
const {
|
||||
visible,
|
||||
windowStart,
|
||||
visibleCount,
|
||||
total,
|
||||
focusedIndex,
|
||||
direction,
|
||||
getKey,
|
||||
renderItem,
|
||||
emptyText
|
||||
} = t0;
|
||||
|
||||
type ListProps<T> = Pick<
|
||||
Props<T>,
|
||||
'visibleCount' | 'direction' | 'getKey' | 'renderItem'
|
||||
> & {
|
||||
visible: readonly T[]
|
||||
windowStart: number
|
||||
total: number
|
||||
focusedIndex: number
|
||||
emptyText: string
|
||||
}
|
||||
|
||||
function List<T>({
|
||||
visible,
|
||||
windowStart,
|
||||
visibleCount,
|
||||
total,
|
||||
focusedIndex,
|
||||
direction,
|
||||
getKey,
|
||||
renderItem,
|
||||
emptyText,
|
||||
}: ListProps<T>): React.ReactNode {
|
||||
if (visible.length === 0) {
|
||||
let t1;
|
||||
if ($[0] !== emptyText) {
|
||||
t1 = <Text dimColor={true}>{emptyText}</Text>;
|
||||
$[0] = emptyText;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== t1 || $[3] !== visibleCount) {
|
||||
t2 = <Box height={visibleCount} flexShrink={0}>{t1}</Box>;
|
||||
$[2] = t1;
|
||||
$[3] = visibleCount;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
return (
|
||||
<Box height={visibleCount} flexShrink={0}>
|
||||
<Text dimColor>{emptyText}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
let t1;
|
||||
if ($[5] !== direction || $[6] !== focusedIndex || $[7] !== getKey || $[8] !== renderItem || $[9] !== total || $[10] !== visible || $[11] !== visibleCount || $[12] !== windowStart) {
|
||||
let t2;
|
||||
if ($[14] !== direction || $[15] !== focusedIndex || $[16] !== getKey || $[17] !== renderItem || $[18] !== total || $[19] !== visible.length || $[20] !== visibleCount || $[21] !== windowStart) {
|
||||
t2 = (item, i) => {
|
||||
const actualIndex = windowStart + i;
|
||||
const isFocused = actualIndex === focusedIndex;
|
||||
const atLowEdge = i === 0 && windowStart > 0;
|
||||
const atHighEdge = i === visible.length - 1 && windowStart + visibleCount < total;
|
||||
return <ListItem key={getKey(item)} isFocused={isFocused} showScrollUp={direction === "up" ? atHighEdge : atLowEdge} showScrollDown={direction === "up" ? atLowEdge : atHighEdge} styled={false}>{renderItem(item, isFocused)}</ListItem>;
|
||||
};
|
||||
$[14] = direction;
|
||||
$[15] = focusedIndex;
|
||||
$[16] = getKey;
|
||||
$[17] = renderItem;
|
||||
$[18] = total;
|
||||
$[19] = visible.length;
|
||||
$[20] = visibleCount;
|
||||
$[21] = windowStart;
|
||||
$[22] = t2;
|
||||
} else {
|
||||
t2 = $[22];
|
||||
}
|
||||
t1 = visible.map(t2);
|
||||
$[5] = direction;
|
||||
$[6] = focusedIndex;
|
||||
$[7] = getKey;
|
||||
$[8] = renderItem;
|
||||
$[9] = total;
|
||||
$[10] = visible;
|
||||
$[11] = visibleCount;
|
||||
$[12] = windowStart;
|
||||
$[13] = t1;
|
||||
} else {
|
||||
t1 = $[13];
|
||||
}
|
||||
const rows = t1;
|
||||
const t2 = direction === "up" ? "column-reverse" : "column";
|
||||
let t3;
|
||||
if ($[23] !== rows || $[24] !== t2 || $[25] !== visibleCount) {
|
||||
t3 = <Box height={visibleCount} flexShrink={0} flexDirection={t2}>{rows}</Box>;
|
||||
$[23] = rows;
|
||||
$[24] = t2;
|
||||
$[25] = visibleCount;
|
||||
$[26] = t3;
|
||||
} else {
|
||||
t3 = $[26];
|
||||
}
|
||||
return t3;
|
||||
|
||||
const rows = visible.map((item, i) => {
|
||||
const actualIndex = windowStart + i
|
||||
const isFocused = actualIndex === focusedIndex
|
||||
const atLowEdge = i === 0 && windowStart > 0
|
||||
const atHighEdge =
|
||||
i === visible.length - 1 && windowStart + visibleCount! < total
|
||||
return (
|
||||
<ListItem
|
||||
key={getKey(item)}
|
||||
isFocused={isFocused}
|
||||
showScrollUp={direction === 'up' ? atHighEdge : atLowEdge}
|
||||
showScrollDown={direction === 'up' ? atLowEdge : atHighEdge}
|
||||
styled={false}
|
||||
>
|
||||
{renderItem(item, isFocused)}
|
||||
</ListItem>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<Box
|
||||
height={visibleCount}
|
||||
flexShrink={0}
|
||||
flexDirection={direction === 'up' ? 'column-reverse' : 'column'}
|
||||
>
|
||||
{rows}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function firstWord(s: string): string {
|
||||
const i = s.indexOf(' ');
|
||||
return i === -1 ? s : s.slice(0, i);
|
||||
const i = s.indexOf(' ')
|
||||
return i === -1 ? s : s.slice(0, i)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import Text from '../../ink/components/Text.js';
|
||||
import React from 'react'
|
||||
import Text from '../../ink/components/Text.js'
|
||||
|
||||
type Props = {
|
||||
/** The key or chord to display (e.g., "ctrl+o", "Enter", "↑/↓") */
|
||||
shortcut: string;
|
||||
shortcut: string
|
||||
/** The action the key performs (e.g., "expand", "select", "navigate") */
|
||||
action: string;
|
||||
action: string
|
||||
/** Whether to wrap the hint in parentheses. Default: false */
|
||||
parens?: boolean;
|
||||
parens?: boolean
|
||||
/** Whether to render the shortcut in bold. Default: false */
|
||||
bold?: boolean;
|
||||
};
|
||||
bold?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a keyboard shortcut hint like "ctrl+o to expand" or "(tab to toggle)"
|
||||
@@ -35,46 +35,24 @@ type Props = {
|
||||
* </Byline>
|
||||
* </Text>
|
||||
*/
|
||||
export function KeyboardShortcutHint(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
shortcut,
|
||||
action,
|
||||
parens: t1,
|
||||
bold: t2
|
||||
} = t0;
|
||||
const parens = t1 === undefined ? false : t1;
|
||||
const bold = t2 === undefined ? false : t2;
|
||||
let t3;
|
||||
if ($[0] !== bold || $[1] !== shortcut) {
|
||||
t3 = bold ? <Text bold={true}>{shortcut}</Text> : shortcut;
|
||||
$[0] = bold;
|
||||
$[1] = shortcut;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t3 = $[2];
|
||||
}
|
||||
const shortcutText = t3;
|
||||
export function KeyboardShortcutHint({
|
||||
shortcut,
|
||||
action,
|
||||
parens = false,
|
||||
bold = false,
|
||||
}: Props): React.ReactNode {
|
||||
const shortcutText = bold ? <Text bold>{shortcut}</Text> : shortcut
|
||||
|
||||
if (parens) {
|
||||
let t4;
|
||||
if ($[3] !== action || $[4] !== shortcutText) {
|
||||
t4 = <Text>({shortcutText} to {action})</Text>;
|
||||
$[3] = action;
|
||||
$[4] = shortcutText;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
return (
|
||||
<Text>
|
||||
({shortcutText} to {action})
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== action || $[7] !== shortcutText) {
|
||||
t4 = <Text>{shortcutText} to {action}</Text>;
|
||||
$[6] = action;
|
||||
$[7] = shortcutText;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
return t4;
|
||||
return (
|
||||
<Text>
|
||||
{shortcutText} to {action}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import figures from 'figures'
|
||||
import type { ReactNode } from 'react'
|
||||
import React from 'react'
|
||||
import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
type ListItemProps = {
|
||||
/**
|
||||
* Whether this item is currently focused (keyboard selection).
|
||||
* Shows the pointer indicator (❯) when true.
|
||||
*/
|
||||
isFocused: boolean;
|
||||
isFocused: boolean
|
||||
|
||||
/**
|
||||
* Whether this item is selected (chosen/checked).
|
||||
* Shows the checkmark indicator (✓) when true.
|
||||
* @default false
|
||||
*/
|
||||
isSelected?: boolean;
|
||||
isSelected?: boolean
|
||||
|
||||
/**
|
||||
* The content to display for this item.
|
||||
*/
|
||||
children: ReactNode;
|
||||
children: ReactNode
|
||||
|
||||
/**
|
||||
* Optional description text displayed below the main content.
|
||||
*/
|
||||
description?: string;
|
||||
description?: string
|
||||
|
||||
/**
|
||||
* Show a down arrow indicator instead of pointer (for scroll hints).
|
||||
* Only applies when not focused.
|
||||
*/
|
||||
showScrollDown?: boolean;
|
||||
showScrollDown?: boolean
|
||||
|
||||
/**
|
||||
* Show an up arrow indicator instead of pointer (for scroll hints).
|
||||
* Only applies when not focused.
|
||||
*/
|
||||
showScrollUp?: boolean;
|
||||
showScrollUp?: boolean
|
||||
|
||||
/**
|
||||
* Whether to apply automatic styling to the children based on focus/selection state.
|
||||
@@ -46,21 +46,21 @@ type ListItemProps = {
|
||||
* - When false: children are rendered as-is, allowing custom styling
|
||||
* @default true
|
||||
*/
|
||||
styled?: boolean;
|
||||
styled?: boolean
|
||||
|
||||
/**
|
||||
* Whether this item is disabled. Disabled items show dimmed text and no indicators.
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
disabled?: boolean
|
||||
|
||||
/**
|
||||
* Whether this ListItem should declare the terminal cursor position.
|
||||
* Set false when a child (e.g. BaseTextInput) declares its own cursor.
|
||||
* @default true
|
||||
*/
|
||||
declareCursor?: boolean;
|
||||
};
|
||||
declareCursor?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A list item component for selection UIs (dropdowns, multi-selects, menus).
|
||||
@@ -101,143 +101,88 @@ type ListItemProps = {
|
||||
* <Text color="claude">Custom styled content</Text>
|
||||
* </ListItem>
|
||||
*/
|
||||
export function ListItem(t0) {
|
||||
const $ = _c(32);
|
||||
const {
|
||||
isFocused,
|
||||
isSelected: t1,
|
||||
children,
|
||||
description,
|
||||
showScrollDown,
|
||||
showScrollUp,
|
||||
styled: t2,
|
||||
disabled: t3,
|
||||
declareCursor
|
||||
} = t0;
|
||||
const isSelected = t1 === undefined ? false : t1;
|
||||
const styled = t2 === undefined ? true : t2;
|
||||
const disabled = t3 === undefined ? false : t3;
|
||||
let t4;
|
||||
if ($[0] !== disabled || $[1] !== isFocused || $[2] !== showScrollDown || $[3] !== showScrollUp) {
|
||||
t4 = function renderIndicator() {
|
||||
if (disabled) {
|
||||
return <Text> </Text>;
|
||||
}
|
||||
if (isFocused) {
|
||||
return <Text color="suggestion">{figures.pointer}</Text>;
|
||||
}
|
||||
if (showScrollDown) {
|
||||
return <Text dimColor={true}>{figures.arrowDown}</Text>;
|
||||
}
|
||||
if (showScrollUp) {
|
||||
return <Text dimColor={true}>{figures.arrowUp}</Text>;
|
||||
}
|
||||
return <Text> </Text>;
|
||||
};
|
||||
$[0] = disabled;
|
||||
$[1] = isFocused;
|
||||
$[2] = showScrollDown;
|
||||
$[3] = showScrollUp;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
export function ListItem({
|
||||
isFocused,
|
||||
isSelected = false,
|
||||
children,
|
||||
description,
|
||||
showScrollDown,
|
||||
showScrollUp,
|
||||
styled = true,
|
||||
disabled = false,
|
||||
declareCursor,
|
||||
}: ListItemProps): React.ReactNode {
|
||||
// Determine which indicator to show
|
||||
function renderIndicator(): ReactNode {
|
||||
if (disabled) {
|
||||
return <Text> </Text>
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
return <Text color="suggestion">{figures.pointer}</Text>
|
||||
}
|
||||
|
||||
if (showScrollDown) {
|
||||
return <Text dimColor>{figures.arrowDown}</Text>
|
||||
}
|
||||
|
||||
if (showScrollUp) {
|
||||
return <Text dimColor>{figures.arrowUp}</Text>
|
||||
}
|
||||
|
||||
return <Text> </Text>
|
||||
}
|
||||
const renderIndicator = t4;
|
||||
let t5;
|
||||
if ($[5] !== disabled || $[6] !== isFocused || $[7] !== isSelected || $[8] !== styled) {
|
||||
const getTextColor = function getTextColor() {
|
||||
if (disabled) {
|
||||
return "inactive";
|
||||
}
|
||||
if (!styled) {
|
||||
return;
|
||||
}
|
||||
if (isSelected) {
|
||||
return "success";
|
||||
}
|
||||
if (isFocused) {
|
||||
return "suggestion";
|
||||
}
|
||||
};
|
||||
t5 = getTextColor();
|
||||
$[5] = disabled;
|
||||
$[6] = isFocused;
|
||||
$[7] = isSelected;
|
||||
$[8] = styled;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
|
||||
// Determine text color based on state
|
||||
function getTextColor(): 'success' | 'suggestion' | 'inactive' | undefined {
|
||||
if (disabled) {
|
||||
return 'inactive'
|
||||
}
|
||||
|
||||
if (!styled) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
return 'suggestion'
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
const textColor = t5;
|
||||
const t6 = isFocused && !disabled && declareCursor !== false;
|
||||
let t7;
|
||||
if ($[10] !== t6) {
|
||||
t7 = {
|
||||
line: 0,
|
||||
column: 0,
|
||||
active: t6
|
||||
};
|
||||
$[10] = t6;
|
||||
$[11] = t7;
|
||||
} else {
|
||||
t7 = $[11];
|
||||
}
|
||||
const cursorRef = useDeclaredCursor(t7);
|
||||
let t8;
|
||||
if ($[12] !== renderIndicator) {
|
||||
t8 = renderIndicator();
|
||||
$[12] = renderIndicator;
|
||||
$[13] = t8;
|
||||
} else {
|
||||
t8 = $[13];
|
||||
}
|
||||
let t9;
|
||||
if ($[14] !== children || $[15] !== disabled || $[16] !== styled || $[17] !== textColor) {
|
||||
t9 = styled ? <Text color={textColor} dimColor={disabled}>{children}</Text> : children;
|
||||
$[14] = children;
|
||||
$[15] = disabled;
|
||||
$[16] = styled;
|
||||
$[17] = textColor;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
t9 = $[18];
|
||||
}
|
||||
let t10;
|
||||
if ($[19] !== disabled || $[20] !== isSelected) {
|
||||
t10 = isSelected && !disabled && <Text color="success">{figures.tick}</Text>;
|
||||
$[19] = disabled;
|
||||
$[20] = isSelected;
|
||||
$[21] = t10;
|
||||
} else {
|
||||
t10 = $[21];
|
||||
}
|
||||
let t11;
|
||||
if ($[22] !== t10 || $[23] !== t8 || $[24] !== t9) {
|
||||
t11 = <Box flexDirection="row" gap={1}>{t8}{t9}{t10}</Box>;
|
||||
$[22] = t10;
|
||||
$[23] = t8;
|
||||
$[24] = t9;
|
||||
$[25] = t11;
|
||||
} else {
|
||||
t11 = $[25];
|
||||
}
|
||||
let t12;
|
||||
if ($[26] !== description) {
|
||||
t12 = description && <Box paddingLeft={2}><Text color="inactive">{description}</Text></Box>;
|
||||
$[26] = description;
|
||||
$[27] = t12;
|
||||
} else {
|
||||
t12 = $[27];
|
||||
}
|
||||
let t13;
|
||||
if ($[28] !== cursorRef || $[29] !== t11 || $[30] !== t12) {
|
||||
t13 = <Box ref={cursorRef} flexDirection="column">{t11}{t12}</Box>;
|
||||
$[28] = cursorRef;
|
||||
$[29] = t11;
|
||||
$[30] = t12;
|
||||
$[31] = t13;
|
||||
} else {
|
||||
t13 = $[31];
|
||||
}
|
||||
return t13;
|
||||
|
||||
const textColor = getTextColor()
|
||||
|
||||
// Park the native terminal cursor on the pointer indicator so screen
|
||||
// readers / magnifiers track the focused item. (0,0) is the top-left of
|
||||
// this Box, where the pointer renders.
|
||||
const cursorRef = useDeclaredCursor({
|
||||
line: 0,
|
||||
column: 0,
|
||||
active: isFocused && !disabled && declareCursor !== false,
|
||||
})
|
||||
|
||||
return (
|
||||
<Box ref={cursorRef} flexDirection="column">
|
||||
<Box flexDirection="row" gap={1}>
|
||||
{renderIndicator()}
|
||||
{styled ? (
|
||||
<Text color={textColor} dimColor={disabled}>
|
||||
{children}
|
||||
</Text>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
{isSelected && !disabled && <Text color="success">{figures.tick}</Text>}
|
||||
</Box>
|
||||
{description && (
|
||||
<Box paddingLeft={2}>
|
||||
<Text color="inactive">{description}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { Spinner } from '../Spinner.js';
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { Spinner } from '../Spinner.js'
|
||||
|
||||
type LoadingStateProps = {
|
||||
/**
|
||||
* The loading message to display next to the spinner.
|
||||
*/
|
||||
message: string;
|
||||
message: string
|
||||
|
||||
/**
|
||||
* Display the message in bold.
|
||||
* @default false
|
||||
*/
|
||||
bold?: boolean;
|
||||
bold?: boolean
|
||||
|
||||
/**
|
||||
* Display the message in dimmed color.
|
||||
* @default false
|
||||
*/
|
||||
dimColor?: boolean;
|
||||
dimColor?: boolean
|
||||
|
||||
/**
|
||||
* Optional subtitle displayed below the main message.
|
||||
*/
|
||||
subtitle?: string;
|
||||
};
|
||||
subtitle?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A spinner with loading message for async operations.
|
||||
@@ -45,49 +45,22 @@ type LoadingStateProps = {
|
||||
* subtitle="Fetching your Claude Code sessions..."
|
||||
* />
|
||||
*/
|
||||
export function LoadingState(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
message,
|
||||
bold: t1,
|
||||
dimColor: t2,
|
||||
subtitle
|
||||
} = t0;
|
||||
const bold = t1 === undefined ? false : t1;
|
||||
const dimColor = t2 === undefined ? false : t2;
|
||||
let t3;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Spinner />;
|
||||
$[0] = t3;
|
||||
} else {
|
||||
t3 = $[0];
|
||||
}
|
||||
let t4;
|
||||
if ($[1] !== bold || $[2] !== dimColor || $[3] !== message) {
|
||||
t4 = <Box flexDirection="row">{t3}<Text bold={bold} dimColor={dimColor}>{" "}{message}</Text></Box>;
|
||||
$[1] = bold;
|
||||
$[2] = dimColor;
|
||||
$[3] = message;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
let t5;
|
||||
if ($[5] !== subtitle) {
|
||||
t5 = subtitle && <Text dimColor={true}>{subtitle}</Text>;
|
||||
$[5] = subtitle;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
}
|
||||
let t6;
|
||||
if ($[7] !== t4 || $[8] !== t5) {
|
||||
t6 = <Box flexDirection="column">{t4}{t5}</Box>;
|
||||
$[7] = t4;
|
||||
$[8] = t5;
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
return t6;
|
||||
export function LoadingState({
|
||||
message,
|
||||
bold = false,
|
||||
dimColor = false,
|
||||
subtitle,
|
||||
}: LoadingStateProps): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="row">
|
||||
<Spinner />
|
||||
<Text bold={bold} dimColor={dimColor}>
|
||||
{' '}
|
||||
{message}
|
||||
</Text>
|
||||
</Box>
|
||||
{subtitle && <Text dimColor>{subtitle}</Text>}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { useIsInsideModal } from '../../context/modalContext.js';
|
||||
import { Box } from '../../ink.js';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
import { Divider } from './Divider.js';
|
||||
import React from 'react'
|
||||
import { useIsInsideModal } from '../../context/modalContext.js'
|
||||
import { Box } from '../../ink.js'
|
||||
import type { Theme } from '../../utils/theme.js'
|
||||
import { Divider } from './Divider.js'
|
||||
|
||||
type PaneProps = {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode
|
||||
/**
|
||||
* Theme color for the top border line.
|
||||
*/
|
||||
color?: keyof Theme;
|
||||
};
|
||||
color?: keyof Theme
|
||||
}
|
||||
|
||||
/**
|
||||
* A pane — a region of the terminal that appears below the REPL prompt,
|
||||
@@ -30,47 +30,28 @@ type PaneProps = {
|
||||
* <Tabs title="Sandbox:">...</Tabs>
|
||||
* </Pane>
|
||||
*/
|
||||
export function Pane(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
children,
|
||||
color
|
||||
} = t0;
|
||||
export function Pane({ children, color }: PaneProps): React.ReactNode {
|
||||
// When rendered inside FullscreenLayout's modal slot, its ▔ divider IS
|
||||
// the frame. Skip our own Divider (would double-frame) and the extra top
|
||||
// padding. This lets slash-command screens that wrap in Pane (e.g.
|
||||
// /model → ModelPicker) route through the modal slot unchanged.
|
||||
if (useIsInsideModal()) {
|
||||
let t1;
|
||||
if ($[0] !== children) {
|
||||
t1 = <Box flexDirection="column" paddingX={1} flexShrink={0}>{children}</Box>;
|
||||
$[0] = children;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
// flexShrink=0: the modal slot's absolute Box has no explicit height
|
||||
// (grows to fit, maxHeight cap). With flexGrow=1, re-renders cause
|
||||
// yoga to resolve this Box's height to 0 against the undetermined
|
||||
// parent — /permissions body blanks on Down arrow. See #23592.
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1} flexShrink={0}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
let t1;
|
||||
if ($[2] !== color) {
|
||||
t1 = <Divider color={color} />;
|
||||
$[2] = color;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
let t2;
|
||||
if ($[4] !== children) {
|
||||
t2 = <Box flexDirection="column" paddingX={2}>{children}</Box>;
|
||||
$[4] = children;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
let t3;
|
||||
if ($[6] !== t1 || $[7] !== t2) {
|
||||
t3 = <Box flexDirection="column" paddingTop={1}>{t1}{t2}</Box>;
|
||||
$[6] = t1;
|
||||
$[7] = t2;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
return t3;
|
||||
return (
|
||||
<Box flexDirection="column" paddingTop={1}>
|
||||
<Divider color={color} />
|
||||
<Box flexDirection="column" paddingX={2}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,85 +1,54 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Text } from '../../ink.js';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
import React from 'react'
|
||||
import { Text } from '../../ink.js'
|
||||
import type { Theme } from '../../utils/theme.js'
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* How much progress to display, between 0 and 1 inclusive
|
||||
*/
|
||||
ratio: number; // [0, 1]
|
||||
ratio: number // [0, 1]
|
||||
|
||||
/**
|
||||
* How many characters wide to draw the progress bar
|
||||
*/
|
||||
width: number; // how many characters wide
|
||||
width: number // how many characters wide
|
||||
|
||||
/**
|
||||
* Optional color for the filled portion of the bar
|
||||
*/
|
||||
fillColor?: keyof Theme;
|
||||
fillColor?: keyof Theme
|
||||
|
||||
/**
|
||||
* Optional color for the empty portion of the bar
|
||||
*/
|
||||
emptyColor?: keyof Theme;
|
||||
};
|
||||
const BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
|
||||
export function ProgressBar(t0) {
|
||||
const $ = _c(13);
|
||||
const {
|
||||
ratio: inputRatio,
|
||||
width,
|
||||
fillColor,
|
||||
emptyColor
|
||||
} = t0;
|
||||
const ratio = Math.min(1, Math.max(0, inputRatio));
|
||||
const whole = Math.floor(ratio * width);
|
||||
let t1;
|
||||
if ($[0] !== whole) {
|
||||
t1 = BLOCKS[BLOCKS.length - 1].repeat(whole);
|
||||
$[0] = whole;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let segments;
|
||||
if ($[2] !== ratio || $[3] !== t1 || $[4] !== whole || $[5] !== width) {
|
||||
segments = [t1];
|
||||
if (whole < width) {
|
||||
const remainder = ratio * width - whole;
|
||||
const middle = Math.floor(remainder * BLOCKS.length);
|
||||
segments.push(BLOCKS[middle]);
|
||||
const empty = width - whole - 1;
|
||||
if (empty > 0) {
|
||||
let t2;
|
||||
if ($[7] !== empty) {
|
||||
t2 = BLOCKS[0].repeat(empty);
|
||||
$[7] = empty;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
segments.push(t2);
|
||||
}
|
||||
}
|
||||
$[2] = ratio;
|
||||
$[3] = t1;
|
||||
$[4] = whole;
|
||||
$[5] = width;
|
||||
$[6] = segments;
|
||||
} else {
|
||||
segments = $[6];
|
||||
}
|
||||
const t2 = segments.join("");
|
||||
let t3;
|
||||
if ($[9] !== emptyColor || $[10] !== fillColor || $[11] !== t2) {
|
||||
t3 = <Text color={fillColor} backgroundColor={emptyColor}>{t2}</Text>;
|
||||
$[9] = emptyColor;
|
||||
$[10] = fillColor;
|
||||
$[11] = t2;
|
||||
$[12] = t3;
|
||||
} else {
|
||||
t3 = $[12];
|
||||
}
|
||||
return t3;
|
||||
emptyColor?: keyof Theme
|
||||
}
|
||||
|
||||
const BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']
|
||||
|
||||
export function ProgressBar({
|
||||
ratio: inputRatio,
|
||||
width,
|
||||
fillColor,
|
||||
emptyColor,
|
||||
}: Props): React.ReactNode {
|
||||
const ratio = Math.min(1, Math.max(0, inputRatio))
|
||||
const whole = Math.floor(ratio * width)
|
||||
const segments = [BLOCKS[BLOCKS.length - 1]!.repeat(whole)]
|
||||
if (whole < width) {
|
||||
const remainder = ratio * width - whole
|
||||
const middle = Math.floor(remainder * BLOCKS.length)
|
||||
segments.push(BLOCKS[middle]!)
|
||||
|
||||
const empty = width - whole - 1
|
||||
if (empty > 0) {
|
||||
segments.push(BLOCKS[0]!.repeat(empty))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Text color={fillColor} backgroundColor={emptyColor}>
|
||||
{segments.join('')}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,79 +1,45 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { useTerminalViewport } from '../../ink/hooks/use-terminal-viewport.js';
|
||||
import { Box, type DOMElement, measureElement } from '../../ink.js';
|
||||
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { useTerminalViewport } from '../../ink/hooks/use-terminal-viewport.js'
|
||||
import { Box, type DOMElement, measureElement } from '../../ink.js'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
lock?: 'always' | 'offscreen';
|
||||
};
|
||||
export function Ratchet(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
children,
|
||||
lock: t1
|
||||
} = t0;
|
||||
const lock = t1 === undefined ? "always" : t1;
|
||||
const [viewportRef, t2] = useTerminalViewport();
|
||||
const {
|
||||
isVisible
|
||||
} = t2;
|
||||
const {
|
||||
rows
|
||||
} = useTerminalSize();
|
||||
const innerRef = useRef(null);
|
||||
const maxHeight = useRef(0);
|
||||
const [minHeight, setMinHeight] = useState(0);
|
||||
let t3;
|
||||
if ($[0] !== viewportRef) {
|
||||
t3 = el => {
|
||||
viewportRef(el);
|
||||
};
|
||||
$[0] = viewportRef;
|
||||
$[1] = t3;
|
||||
} else {
|
||||
t3 = $[1];
|
||||
}
|
||||
const outerRef = t3;
|
||||
const engaged = lock === "always" || !isVisible;
|
||||
let t4;
|
||||
if ($[2] !== rows) {
|
||||
t4 = () => {
|
||||
if (!innerRef.current) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
height
|
||||
} = measureElement(innerRef.current);
|
||||
if (height > maxHeight.current) {
|
||||
maxHeight.current = Math.min(height, rows);
|
||||
setMinHeight(maxHeight.current);
|
||||
}
|
||||
};
|
||||
$[2] = rows;
|
||||
$[3] = t4;
|
||||
} else {
|
||||
t4 = $[3];
|
||||
}
|
||||
useLayoutEffect(t4);
|
||||
const t5 = engaged ? minHeight : undefined;
|
||||
let t6;
|
||||
if ($[4] !== children) {
|
||||
t6 = <Box ref={innerRef} flexDirection="column">{children}</Box>;
|
||||
$[4] = children;
|
||||
$[5] = t6;
|
||||
} else {
|
||||
t6 = $[5];
|
||||
}
|
||||
let t7;
|
||||
if ($[6] !== outerRef || $[7] !== t5 || $[8] !== t6) {
|
||||
t7 = <Box minHeight={t5} ref={outerRef}>{t6}</Box>;
|
||||
$[6] = outerRef;
|
||||
$[7] = t5;
|
||||
$[8] = t6;
|
||||
$[9] = t7;
|
||||
} else {
|
||||
t7 = $[9];
|
||||
}
|
||||
return t7;
|
||||
children: React.ReactNode
|
||||
lock?: 'always' | 'offscreen'
|
||||
}
|
||||
|
||||
export function Ratchet({ children, lock = 'always' }: Props): React.ReactNode {
|
||||
const [viewportRef, { isVisible }] = useTerminalViewport()
|
||||
const { rows } = useTerminalSize()
|
||||
const innerRef = useRef<DOMElement | null>(null)
|
||||
const maxHeight = useRef(0)
|
||||
const [minHeight, setMinHeight] = useState(0)
|
||||
|
||||
const outerRef = useCallback(
|
||||
(el: DOMElement | null) => {
|
||||
viewportRef(el)
|
||||
},
|
||||
[viewportRef],
|
||||
)
|
||||
|
||||
const engaged = lock === 'always' || !isVisible
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!innerRef.current) {
|
||||
return
|
||||
}
|
||||
const { height } = measureElement(innerRef.current)
|
||||
if (height > maxHeight.current) {
|
||||
maxHeight.current = Math.min(height, rows)
|
||||
setMinHeight(maxHeight.current)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Box minHeight={engaged ? minHeight : undefined} ref={outerRef}>
|
||||
<Box ref={innerRef} flexDirection="column">
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React from 'react';
|
||||
import { Text } from '../../ink.js';
|
||||
type Status = 'success' | 'error' | 'warning' | 'info' | 'pending' | 'loading';
|
||||
import figures from 'figures'
|
||||
import React from 'react'
|
||||
import { Text } from '../../ink.js'
|
||||
|
||||
type Status = 'success' | 'error' | 'warning' | 'info' | 'pending' | 'loading'
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* The status to display. Determines both the icon and color.
|
||||
@@ -14,42 +15,28 @@ type Props = {
|
||||
* - `pending`: Dimmed circle (○)
|
||||
* - `loading`: Dimmed ellipsis (…)
|
||||
*/
|
||||
status: Status;
|
||||
status: Status
|
||||
/**
|
||||
* Include a trailing space after the icon. Useful when followed by text.
|
||||
* @default false
|
||||
*/
|
||||
withSpace?: boolean;
|
||||
};
|
||||
const STATUS_CONFIG: Record<Status, {
|
||||
icon: string;
|
||||
color: 'success' | 'error' | 'warning' | 'suggestion' | undefined;
|
||||
}> = {
|
||||
success: {
|
||||
icon: figures.tick,
|
||||
color: 'success'
|
||||
},
|
||||
error: {
|
||||
icon: figures.cross,
|
||||
color: 'error'
|
||||
},
|
||||
warning: {
|
||||
icon: figures.warning,
|
||||
color: 'warning'
|
||||
},
|
||||
info: {
|
||||
icon: figures.info,
|
||||
color: 'suggestion'
|
||||
},
|
||||
pending: {
|
||||
icon: figures.circle,
|
||||
color: undefined
|
||||
},
|
||||
loading: {
|
||||
icon: '…',
|
||||
color: undefined
|
||||
withSpace?: boolean
|
||||
}
|
||||
|
||||
const STATUS_CONFIG: Record<
|
||||
Status,
|
||||
{
|
||||
icon: string
|
||||
color: 'success' | 'error' | 'warning' | 'suggestion' | undefined
|
||||
}
|
||||
};
|
||||
> = {
|
||||
success: { icon: figures.tick, color: 'success' },
|
||||
error: { icon: figures.cross, color: 'error' },
|
||||
warning: { icon: figures.warning, color: 'warning' },
|
||||
info: { icon: figures.info, color: 'suggestion' },
|
||||
pending: { icon: figures.circle, color: undefined },
|
||||
loading: { icon: '…', color: undefined },
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a status indicator icon with appropriate color.
|
||||
@@ -69,26 +56,16 @@ const STATUS_CONFIG: Record<Status, {
|
||||
* Waiting for response
|
||||
* </Text>
|
||||
*/
|
||||
export function StatusIcon(t0) {
|
||||
const $ = _c(5);
|
||||
const {
|
||||
status,
|
||||
withSpace: t1
|
||||
} = t0;
|
||||
const withSpace = t1 === undefined ? false : t1;
|
||||
const config = STATUS_CONFIG[status];
|
||||
const t2 = !config.color;
|
||||
const t3 = withSpace && " ";
|
||||
let t4;
|
||||
if ($[0] !== config.color || $[1] !== config.icon || $[2] !== t2 || $[3] !== t3) {
|
||||
t4 = <Text color={config.color} dimColor={t2}>{config.icon}{t3}</Text>;
|
||||
$[0] = config.color;
|
||||
$[1] = config.icon;
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
return t4;
|
||||
export function StatusIcon({
|
||||
status,
|
||||
withSpace = false,
|
||||
}: Props): React.ReactNode {
|
||||
const config = STATUS_CONFIG[status]
|
||||
|
||||
return (
|
||||
<Text color={config.color} dimColor={!config.color}>
|
||||
{config.icon}
|
||||
{withSpace && ' '}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,28 +1,37 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useIsInsideModal, useModalScrollRef } from '../../context/modalContext.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import ScrollBox from '../../ink/components/ScrollBox.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
useIsInsideModal,
|
||||
useModalScrollRef,
|
||||
} from '../../context/modalContext.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import ScrollBox from '../../ink/components/ScrollBox.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import type { Theme } from '../../utils/theme.js'
|
||||
|
||||
type TabsProps = {
|
||||
children: Array<React.ReactElement<TabProps>>;
|
||||
title?: string;
|
||||
color?: keyof Theme;
|
||||
defaultTab?: string;
|
||||
hidden?: boolean;
|
||||
useFullWidth?: boolean;
|
||||
children: Array<React.ReactElement<TabProps>>
|
||||
title?: string
|
||||
color?: keyof Theme
|
||||
defaultTab?: string
|
||||
hidden?: boolean
|
||||
useFullWidth?: boolean
|
||||
/** Controlled mode: current selected tab id/title */
|
||||
selectedTab?: string;
|
||||
selectedTab?: string
|
||||
/** Controlled mode: callback when tab changes */
|
||||
onTabChange?: (tabId: string) => void;
|
||||
onTabChange?: (tabId: string) => void
|
||||
/** Optional banner to display below tabs header */
|
||||
banner?: React.ReactNode;
|
||||
banner?: React.ReactNode
|
||||
/** Disable keyboard navigation (e.g. when a child component handles arrow keys) */
|
||||
disableNavigation?: boolean;
|
||||
disableNavigation?: boolean
|
||||
/**
|
||||
* Initial focus state for the tab header row. Defaults to true (header
|
||||
* focused, nav always works). Keep the default for Select/list content —
|
||||
@@ -31,28 +40,30 @@ type TabsProps = {
|
||||
* content actually binds left/right/tab (e.g. enum cycling), and show a
|
||||
* "↑ tabs" footer hint — without it tabs look broken.
|
||||
*/
|
||||
initialHeaderFocused?: boolean;
|
||||
initialHeaderFocused?: boolean
|
||||
/**
|
||||
* Fixed height for the content area. When set, all tabs render within the
|
||||
* same height (overflow hidden) so switching tabs doesn't cause layout
|
||||
* shifts. Shorter tabs get whitespace; taller tabs are clipped.
|
||||
*/
|
||||
contentHeight?: number;
|
||||
contentHeight?: number
|
||||
/**
|
||||
* Let Tab/←/→ switch tabs from focused content. Opt-in since some
|
||||
* content uses those keys; pass a reactive boolean to cede them when
|
||||
* needed. Switching from content focuses the header.
|
||||
*/
|
||||
navFromContent?: boolean;
|
||||
};
|
||||
navFromContent?: boolean
|
||||
}
|
||||
|
||||
type TabsContextValue = {
|
||||
selectedTab: string | undefined;
|
||||
width: number | undefined;
|
||||
headerFocused: boolean;
|
||||
focusHeader: () => void;
|
||||
blurHeader: () => void;
|
||||
registerOptIn: () => () => void;
|
||||
};
|
||||
selectedTab: string | undefined
|
||||
width: number | undefined
|
||||
headerFocused: boolean
|
||||
focusHeader: () => void
|
||||
blurHeader: () => void
|
||||
registerOptIn: () => () => void
|
||||
}
|
||||
|
||||
const TabsContext = createContext<TabsContextValue>({
|
||||
selectedTab: undefined,
|
||||
width: undefined,
|
||||
@@ -61,236 +72,248 @@ const TabsContext = createContext<TabsContextValue>({
|
||||
headerFocused: false,
|
||||
focusHeader: () => {},
|
||||
blurHeader: () => {},
|
||||
registerOptIn: () => () => {}
|
||||
});
|
||||
export function Tabs(t0) {
|
||||
const $ = _c(25);
|
||||
const {
|
||||
title,
|
||||
color,
|
||||
defaultTab,
|
||||
children,
|
||||
hidden,
|
||||
useFullWidth,
|
||||
selectedTab: controlledSelectedTab,
|
||||
onTabChange,
|
||||
banner,
|
||||
disableNavigation,
|
||||
initialHeaderFocused: t1,
|
||||
contentHeight,
|
||||
navFromContent: t2
|
||||
} = t0;
|
||||
const initialHeaderFocused = t1 === undefined ? true : t1;
|
||||
const navFromContent = t2 === undefined ? false : t2;
|
||||
const {
|
||||
columns: terminalWidth
|
||||
} = useTerminalSize();
|
||||
const tabs = children.map(_temp);
|
||||
const defaultTabIndex = defaultTab ? tabs.findIndex(tab => defaultTab === tab[0]) : 0;
|
||||
const isControlled = controlledSelectedTab !== undefined;
|
||||
const [internalSelectedTab, setInternalSelectedTab] = useState(defaultTabIndex !== -1 ? defaultTabIndex : 0);
|
||||
const controlledTabIndex = isControlled ? tabs.findIndex(tab_0 => tab_0[0] === controlledSelectedTab) : -1;
|
||||
const selectedTabIndex = isControlled ? controlledTabIndex !== -1 ? controlledTabIndex : 0 : internalSelectedTab;
|
||||
const modalScrollRef = useModalScrollRef();
|
||||
const [headerFocused, setHeaderFocused] = useState(initialHeaderFocused);
|
||||
let t3;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = () => setHeaderFocused(true);
|
||||
$[0] = t3;
|
||||
} else {
|
||||
t3 = $[0];
|
||||
}
|
||||
const focusHeader = t3;
|
||||
let t4;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = () => setHeaderFocused(false);
|
||||
$[1] = t4;
|
||||
} else {
|
||||
t4 = $[1];
|
||||
}
|
||||
const blurHeader = t4;
|
||||
const [optInCount, setOptInCount] = useState(0);
|
||||
let t5;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = () => {
|
||||
setOptInCount(_temp2);
|
||||
return () => setOptInCount(_temp3);
|
||||
};
|
||||
$[2] = t5;
|
||||
} else {
|
||||
t5 = $[2];
|
||||
}
|
||||
const registerOptIn = t5;
|
||||
const optedIn = optInCount > 0;
|
||||
const handleTabChange = offset => {
|
||||
const newIndex = (selectedTabIndex + tabs.length + offset) % tabs.length;
|
||||
const newTabId = tabs[newIndex]?.[0];
|
||||
registerOptIn: () => () => {},
|
||||
})
|
||||
|
||||
export function Tabs({
|
||||
title,
|
||||
color,
|
||||
defaultTab,
|
||||
children,
|
||||
hidden,
|
||||
useFullWidth,
|
||||
selectedTab: controlledSelectedTab,
|
||||
onTabChange,
|
||||
banner,
|
||||
disableNavigation,
|
||||
initialHeaderFocused = true,
|
||||
contentHeight,
|
||||
navFromContent = false,
|
||||
}: TabsProps): React.ReactNode {
|
||||
const { columns: terminalWidth } = useTerminalSize()
|
||||
const tabs = children.map(child => [
|
||||
child.props.id ?? child.props.title,
|
||||
child.props.title,
|
||||
])
|
||||
const defaultTabIndex = defaultTab
|
||||
? tabs.findIndex(tab => defaultTab === tab[0])
|
||||
: 0
|
||||
|
||||
// Support both controlled and uncontrolled modes
|
||||
const isControlled = controlledSelectedTab !== undefined
|
||||
const [internalSelectedTab, setInternalSelectedTab] = useState(
|
||||
defaultTabIndex !== -1 ? defaultTabIndex : 0,
|
||||
)
|
||||
|
||||
// In controlled mode, find the index of the controlled tab
|
||||
const controlledTabIndex = isControlled
|
||||
? tabs.findIndex(tab => tab[0] === controlledSelectedTab)
|
||||
: -1
|
||||
const selectedTabIndex = isControlled
|
||||
? controlledTabIndex !== -1
|
||||
? controlledTabIndex
|
||||
: 0
|
||||
: internalSelectedTab
|
||||
|
||||
const modalScrollRef = useModalScrollRef()
|
||||
|
||||
// Header focus: left/right/tab only switch tabs when the header row is
|
||||
// focused. Children with interactive content call focusHeader() (via
|
||||
// useTabHeaderFocus) on up-arrow to hand focus back here; down-arrow
|
||||
// returns it. Tabs that never call the hook see no behavior change —
|
||||
// initialHeaderFocused defaults to true so nav always works.
|
||||
const [headerFocused, setHeaderFocused] = useState(initialHeaderFocused)
|
||||
const focusHeader = useCallback(() => setHeaderFocused(true), [])
|
||||
const blurHeader = useCallback(() => setHeaderFocused(false), [])
|
||||
// Count of mounted children using useTabHeaderFocus(). Down-arrow blur and
|
||||
// the ↓ hint only engage when at least one child has opted in — otherwise
|
||||
// pressing down on a legacy tab would strand the user with nav disabled.
|
||||
const [optInCount, setOptInCount] = useState(0)
|
||||
const registerOptIn = useCallback(() => {
|
||||
setOptInCount(n => n + 1)
|
||||
return () => setOptInCount(n => n - 1)
|
||||
}, [])
|
||||
const optedIn = optInCount > 0
|
||||
|
||||
const handleTabChange = (offset: number) => {
|
||||
const newIndex = (selectedTabIndex + tabs.length + offset) % tabs.length
|
||||
const newTabId = tabs[newIndex]?.[0]
|
||||
|
||||
if (isControlled && onTabChange && newTabId) {
|
||||
onTabChange(newTabId);
|
||||
onTabChange(newTabId)
|
||||
} else {
|
||||
setInternalSelectedTab(newIndex);
|
||||
setInternalSelectedTab(newIndex)
|
||||
}
|
||||
setHeaderFocused(true);
|
||||
};
|
||||
const t6 = !hidden && !disableNavigation && headerFocused;
|
||||
let t7;
|
||||
if ($[3] !== t6) {
|
||||
t7 = {
|
||||
context: "Tabs",
|
||||
isActive: t6
|
||||
};
|
||||
$[3] = t6;
|
||||
$[4] = t7;
|
||||
} else {
|
||||
t7 = $[4];
|
||||
// Tab switching is a header action — stay focused so the user can keep
|
||||
// cycling. The newly mounted tab can blur via its own interaction.
|
||||
setHeaderFocused(true)
|
||||
}
|
||||
useKeybindings({
|
||||
"tabs:next": () => handleTabChange(1),
|
||||
"tabs:previous": () => handleTabChange(-1)
|
||||
}, t7);
|
||||
let t8;
|
||||
if ($[5] !== headerFocused || $[6] !== hidden || $[7] !== optedIn) {
|
||||
t8 = e => {
|
||||
if (!headerFocused || !optedIn || hidden) {
|
||||
return;
|
||||
}
|
||||
if (e.key === "down") {
|
||||
e.preventDefault();
|
||||
setHeaderFocused(false);
|
||||
}
|
||||
};
|
||||
$[5] = headerFocused;
|
||||
$[6] = hidden;
|
||||
$[7] = optedIn;
|
||||
$[8] = t8;
|
||||
} else {
|
||||
t8 = $[8];
|
||||
}
|
||||
const handleKeyDown = t8;
|
||||
const t9 = navFromContent && !headerFocused && optedIn && !hidden && !disableNavigation;
|
||||
let t10;
|
||||
if ($[9] !== t9) {
|
||||
t10 = {
|
||||
context: "Tabs",
|
||||
isActive: t9
|
||||
};
|
||||
$[9] = t9;
|
||||
$[10] = t10;
|
||||
} else {
|
||||
t10 = $[10];
|
||||
}
|
||||
useKeybindings({
|
||||
"tabs:next": () => {
|
||||
handleTabChange(1);
|
||||
setHeaderFocused(true);
|
||||
|
||||
useKeybindings(
|
||||
{
|
||||
'tabs:next': () => handleTabChange(1),
|
||||
'tabs:previous': () => handleTabChange(-1),
|
||||
},
|
||||
"tabs:previous": () => {
|
||||
handleTabChange(-1);
|
||||
setHeaderFocused(true);
|
||||
{
|
||||
context: 'Tabs',
|
||||
isActive: !hidden && !disableNavigation && headerFocused,
|
||||
},
|
||||
)
|
||||
|
||||
// When the header is focused, down-arrow returns focus to content. Only
|
||||
// active when the selected tab has opted in via useTabHeaderFocus() —
|
||||
// legacy tabs have nowhere to return focus to.
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!headerFocused || !optedIn || hidden) return
|
||||
if (e.key === 'down') {
|
||||
e.preventDefault()
|
||||
setHeaderFocused(false)
|
||||
}
|
||||
}, t10);
|
||||
const titleWidth = title ? stringWidth(title) + 1 : 0;
|
||||
const tabsWidth = tabs.reduce(_temp4, 0);
|
||||
const usedWidth = titleWidth + tabsWidth;
|
||||
const spacerWidth = useFullWidth ? Math.max(0, terminalWidth - usedWidth) : 0;
|
||||
const contentWidth = useFullWidth ? terminalWidth : undefined;
|
||||
const T0 = Box;
|
||||
const t11 = "column";
|
||||
const t12 = 0;
|
||||
const t13 = true;
|
||||
const t14 = modalScrollRef ? 0 : undefined;
|
||||
const t15 = !hidden && <Box flexDirection="row" gap={1} flexShrink={modalScrollRef ? 0 : undefined}>{title !== undefined && <Text bold={true} color={color}>{title}</Text>}{tabs.map((t16, i) => {
|
||||
const [id, title_0] = t16;
|
||||
const isCurrent = selectedTabIndex === i;
|
||||
const hasColorCursor = color && isCurrent && headerFocused;
|
||||
return <Text key={id} backgroundColor={hasColorCursor ? color : undefined} color={hasColorCursor ? "inverseText" : undefined} inverse={isCurrent && !hasColorCursor} bold={isCurrent}>{" "}{title_0}{" "}</Text>;
|
||||
})}{spacerWidth > 0 && <Text>{" ".repeat(spacerWidth)}</Text>}</Box>;
|
||||
let t17;
|
||||
if ($[11] !== children || $[12] !== contentHeight || $[13] !== contentWidth || $[14] !== hidden || $[15] !== modalScrollRef || $[16] !== selectedTabIndex) {
|
||||
t17 = modalScrollRef ? <Box width={contentWidth} marginTop={hidden ? 0 : 1} flexShrink={0}><ScrollBox key={selectedTabIndex} ref={modalScrollRef} flexDirection="column" flexShrink={0}>{children}</ScrollBox></Box> : <Box width={contentWidth} marginTop={hidden ? 0 : 1} height={contentHeight} overflowY={contentHeight !== undefined ? "hidden" : undefined}>{children}</Box>;
|
||||
$[11] = children;
|
||||
$[12] = contentHeight;
|
||||
$[13] = contentWidth;
|
||||
$[14] = hidden;
|
||||
$[15] = modalScrollRef;
|
||||
$[16] = selectedTabIndex;
|
||||
$[17] = t17;
|
||||
} else {
|
||||
t17 = $[17];
|
||||
}
|
||||
let t18;
|
||||
if ($[18] !== T0 || $[19] !== banner || $[20] !== handleKeyDown || $[21] !== t14 || $[22] !== t15 || $[23] !== t17) {
|
||||
t18 = <T0 flexDirection={t11} tabIndex={t12} autoFocus={t13} onKeyDown={handleKeyDown} flexShrink={t14}>{t15}{banner}{t17}</T0>;
|
||||
$[18] = T0;
|
||||
$[19] = banner;
|
||||
$[20] = handleKeyDown;
|
||||
$[21] = t14;
|
||||
$[22] = t15;
|
||||
$[23] = t17;
|
||||
$[24] = t18;
|
||||
} else {
|
||||
t18 = $[24];
|
||||
}
|
||||
return <TabsContext.Provider value={{
|
||||
selectedTab: tabs[selectedTabIndex][0],
|
||||
width: contentWidth,
|
||||
headerFocused,
|
||||
focusHeader,
|
||||
blurHeader,
|
||||
registerOptIn
|
||||
}}>{t18}</TabsContext.Provider>;
|
||||
}
|
||||
function _temp4(sum, t0) {
|
||||
const [, tabTitle] = t0;
|
||||
return sum + (tabTitle ? stringWidth(tabTitle) : 0) + 2 + 1;
|
||||
}
|
||||
function _temp3(n_0) {
|
||||
return n_0 - 1;
|
||||
}
|
||||
function _temp2(n) {
|
||||
return n + 1;
|
||||
}
|
||||
function _temp(child) {
|
||||
return [child.props.id ?? child.props.title, child.props.title];
|
||||
|
||||
// Opt-in: same tabs:next/previous actions, active from content. Focuses
|
||||
// the header so subsequent presses cycle via the handler above.
|
||||
useKeybindings(
|
||||
{
|
||||
'tabs:next': () => {
|
||||
handleTabChange(1)
|
||||
setHeaderFocused(true)
|
||||
},
|
||||
'tabs:previous': () => {
|
||||
handleTabChange(-1)
|
||||
setHeaderFocused(true)
|
||||
},
|
||||
},
|
||||
{
|
||||
context: 'Tabs',
|
||||
isActive:
|
||||
navFromContent &&
|
||||
!headerFocused &&
|
||||
optedIn &&
|
||||
!hidden &&
|
||||
!disableNavigation,
|
||||
},
|
||||
)
|
||||
|
||||
// Calculate spacing to fill the available width. No keyboard hint in the
|
||||
// header row — content footers own hints (see useTabHeaderFocus docs).
|
||||
const titleWidth = title ? stringWidth(title) + 1 : 0 // +1 for gap
|
||||
const tabsWidth = tabs.reduce(
|
||||
(sum, [, tabTitle]) => sum + (tabTitle ? stringWidth(tabTitle) : 0) + 2 + 1, // +2 for padding, +1 for gap
|
||||
0,
|
||||
)
|
||||
const usedWidth = titleWidth + tabsWidth
|
||||
const spacerWidth = useFullWidth ? Math.max(0, terminalWidth - usedWidth) : 0
|
||||
|
||||
const contentWidth = useFullWidth ? terminalWidth : undefined
|
||||
|
||||
return (
|
||||
<TabsContext.Provider
|
||||
value={{
|
||||
selectedTab: tabs[selectedTabIndex]![0],
|
||||
width: contentWidth,
|
||||
headerFocused,
|
||||
focusHeader,
|
||||
blurHeader,
|
||||
registerOptIn,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
// flexShrink=0 inside modal slot — the modal's absolute Box has no
|
||||
// explicit height (grows to fit, maxHeight cap), so flexGrow=1 here
|
||||
// resolves to 0 on re-render and the body blanks on Down arrow.
|
||||
// See #23592. Outside modal, leave layout alone.
|
||||
flexShrink={modalScrollRef ? 0 : undefined}
|
||||
>
|
||||
{!hidden && (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
flexShrink={modalScrollRef ? 0 : undefined}
|
||||
>
|
||||
{title !== undefined && (
|
||||
<Text bold color={color}>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
{tabs.map(([id, title], i) => {
|
||||
const isCurrent = selectedTabIndex === i
|
||||
const hasColorCursor = color && isCurrent && headerFocused
|
||||
return (
|
||||
<Text
|
||||
key={id}
|
||||
backgroundColor={hasColorCursor ? color : undefined}
|
||||
color={hasColorCursor ? 'inverseText' : undefined}
|
||||
inverse={isCurrent && !hasColorCursor}
|
||||
bold={isCurrent}
|
||||
>
|
||||
{' '}
|
||||
{title}{' '}
|
||||
</Text>
|
||||
)
|
||||
})}
|
||||
{spacerWidth > 0 && <Text>{' '.repeat(spacerWidth)}</Text>}
|
||||
</Box>
|
||||
)}
|
||||
{banner}
|
||||
{modalScrollRef ? (
|
||||
// Inside the modal slot: own the ScrollBox here so the tabs
|
||||
// header row above sits OUTSIDE the scroll area — it can never
|
||||
// scroll off. The ref reaches REPL's ScrollKeybindingHandler via
|
||||
// ModalContext. Keyed by selectedTabIndex → remounts on tab
|
||||
// switch, resetting scrollTop to 0 without scrollTo() timing games.
|
||||
<Box width={contentWidth} marginTop={hidden ? 0 : 1} flexShrink={0}>
|
||||
<ScrollBox
|
||||
key={selectedTabIndex}
|
||||
ref={modalScrollRef}
|
||||
flexDirection="column"
|
||||
flexShrink={0}
|
||||
>
|
||||
{children}
|
||||
</ScrollBox>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
width={contentWidth}
|
||||
marginTop={hidden ? 0 : 1}
|
||||
height={contentHeight}
|
||||
overflowY={contentHeight !== undefined ? 'hidden' : undefined}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</TabsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
type TabProps = {
|
||||
title: string;
|
||||
id?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
export function Tab(t0) {
|
||||
const $ = _c(4);
|
||||
const {
|
||||
title,
|
||||
id,
|
||||
children
|
||||
} = t0;
|
||||
const {
|
||||
selectedTab,
|
||||
width
|
||||
} = useContext(TabsContext);
|
||||
const insideModal = useIsInsideModal();
|
||||
if (selectedTab !== (id ?? title)) {
|
||||
return null;
|
||||
}
|
||||
const t1 = insideModal ? 0 : undefined;
|
||||
let t2;
|
||||
if ($[0] !== children || $[1] !== t1 || $[2] !== width) {
|
||||
t2 = <Box width={width} flexShrink={t1}>{children}</Box>;
|
||||
$[0] = children;
|
||||
$[1] = t1;
|
||||
$[2] = width;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
title: string
|
||||
id?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
export function useTabsWidth() {
|
||||
const {
|
||||
width
|
||||
} = useContext(TabsContext);
|
||||
return width;
|
||||
|
||||
export function Tab({ title, id, children }: TabProps): React.ReactNode {
|
||||
const { selectedTab, width } = useContext(TabsContext)
|
||||
const insideModal = useIsInsideModal()
|
||||
if (selectedTab !== (id ?? title)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box width={width} flexShrink={insideModal ? 0 : undefined}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function useTabsWidth(): number | undefined {
|
||||
const { width } = useContext(TabsContext)
|
||||
return width
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,36 +327,13 @@ export function useTabsWidth() {
|
||||
* no onUpFromFirstItem to recover. Split the component so the hook only runs
|
||||
* when the Select renders.
|
||||
*/
|
||||
export function useTabHeaderFocus() {
|
||||
const $ = _c(6);
|
||||
const {
|
||||
headerFocused,
|
||||
focusHeader,
|
||||
blurHeader,
|
||||
registerOptIn
|
||||
} = useContext(TabsContext);
|
||||
let t0;
|
||||
if ($[0] !== registerOptIn) {
|
||||
t0 = [registerOptIn];
|
||||
$[0] = registerOptIn;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
useEffect(registerOptIn, t0);
|
||||
let t1;
|
||||
if ($[2] !== blurHeader || $[3] !== focusHeader || $[4] !== headerFocused) {
|
||||
t1 = {
|
||||
headerFocused,
|
||||
focusHeader,
|
||||
blurHeader
|
||||
};
|
||||
$[2] = blurHeader;
|
||||
$[3] = focusHeader;
|
||||
$[4] = headerFocused;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
export function useTabHeaderFocus(): {
|
||||
headerFocused: boolean
|
||||
focusHeader: () => void
|
||||
blurHeader: () => void
|
||||
} {
|
||||
const { headerFocused, focusHeader, blurHeader, registerOptIn } =
|
||||
useContext(TabsContext)
|
||||
useEffect(registerOptIn, [registerOptIn])
|
||||
return { headerFocused, focusHeader, blurHeader }
|
||||
}
|
||||
|
||||
@@ -1,169 +1,160 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import useStdin from '../../ink/hooks/use-stdin.js';
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
||||
import { getSystemThemeName, type SystemTheme } from '../../utils/systemTheme.js';
|
||||
import type { ThemeName, ThemeSetting } from '../../utils/theme.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import useStdin from '../../ink/hooks/use-stdin.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import {
|
||||
getSystemThemeName,
|
||||
type SystemTheme,
|
||||
} from '../../utils/systemTheme.js'
|
||||
import type { ThemeName, ThemeSetting } from '../../utils/theme.js'
|
||||
|
||||
type ThemeContextValue = {
|
||||
/** The saved user preference. May be 'auto'. */
|
||||
themeSetting: ThemeSetting;
|
||||
setThemeSetting: (setting: ThemeSetting) => void;
|
||||
setPreviewTheme: (setting: ThemeSetting) => void;
|
||||
savePreview: () => void;
|
||||
cancelPreview: () => void;
|
||||
themeSetting: ThemeSetting
|
||||
setThemeSetting: (setting: ThemeSetting) => void
|
||||
setPreviewTheme: (setting: ThemeSetting) => void
|
||||
savePreview: () => void
|
||||
cancelPreview: () => void
|
||||
/** The resolved theme to render with. Never 'auto'. */
|
||||
currentTheme: ThemeName;
|
||||
};
|
||||
currentTheme: ThemeName
|
||||
}
|
||||
|
||||
// Non-'auto' default so useTheme() works without a provider (tests, tooling).
|
||||
const DEFAULT_THEME: ThemeName = 'dark';
|
||||
const DEFAULT_THEME: ThemeName = 'dark'
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue>({
|
||||
themeSetting: DEFAULT_THEME,
|
||||
setThemeSetting: () => {},
|
||||
setPreviewTheme: () => {},
|
||||
savePreview: () => {},
|
||||
cancelPreview: () => {},
|
||||
currentTheme: DEFAULT_THEME
|
||||
});
|
||||
currentTheme: DEFAULT_THEME,
|
||||
})
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
initialState?: ThemeSetting;
|
||||
onThemeSave?: (setting: ThemeSetting) => void;
|
||||
};
|
||||
children: React.ReactNode
|
||||
initialState?: ThemeSetting
|
||||
onThemeSave?: (setting: ThemeSetting) => void
|
||||
}
|
||||
|
||||
function defaultInitialTheme(): ThemeSetting {
|
||||
return getGlobalConfig().theme;
|
||||
return getGlobalConfig().theme
|
||||
}
|
||||
|
||||
function defaultSaveTheme(setting: ThemeSetting): void {
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
theme: setting
|
||||
}));
|
||||
saveGlobalConfig(current => ({ ...current, theme: setting }))
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
initialState,
|
||||
onThemeSave = defaultSaveTheme
|
||||
onThemeSave = defaultSaveTheme,
|
||||
}: Props) {
|
||||
const [themeSetting, setThemeSetting] = useState(initialState ?? defaultInitialTheme);
|
||||
const [previewTheme, setPreviewTheme] = useState<ThemeSetting | null>(null);
|
||||
const [themeSetting, setThemeSetting] = useState(
|
||||
initialState ?? defaultInitialTheme,
|
||||
)
|
||||
const [previewTheme, setPreviewTheme] = useState<ThemeSetting | null>(null)
|
||||
|
||||
// Track terminal theme for 'auto' resolution. Seeds from $COLORFGBG (or
|
||||
// 'dark' if unset); the OSC 11 watcher corrects it on first poll.
|
||||
const [systemTheme, setSystemTheme] = useState<SystemTheme>(() => (initialState ?? themeSetting) === 'auto' ? getSystemThemeName() : 'dark');
|
||||
const [systemTheme, setSystemTheme] = useState<SystemTheme>(() =>
|
||||
(initialState ?? themeSetting) === 'auto' ? getSystemThemeName() : 'dark',
|
||||
)
|
||||
|
||||
// The setting currently in effect (preview wins while picker is open)
|
||||
const activeSetting = previewTheme ?? themeSetting;
|
||||
const {
|
||||
internal_querier
|
||||
} = useStdin();
|
||||
const activeSetting = previewTheme ?? themeSetting
|
||||
|
||||
const { internal_querier } = useStdin()
|
||||
|
||||
// Watch for live terminal theme changes while 'auto' is active.
|
||||
// Positive feature() pattern so the watcher import is dead-code-eliminated
|
||||
// in external builds.
|
||||
useEffect(() => {
|
||||
if (feature('AUTO_THEME')) {
|
||||
if (activeSetting !== 'auto' || !internal_querier) return;
|
||||
let cleanup: (() => void) | undefined;
|
||||
let cancelled = false;
|
||||
void import('../../utils/systemThemeWatcher.js').then(({
|
||||
watchSystemTheme
|
||||
}) => {
|
||||
if (cancelled) return;
|
||||
cleanup = watchSystemTheme(internal_querier, setSystemTheme);
|
||||
});
|
||||
if (activeSetting !== 'auto' || !internal_querier) return
|
||||
let cleanup: (() => void) | undefined
|
||||
let cancelled = false
|
||||
void import('../../utils/systemThemeWatcher.js').then(
|
||||
({ watchSystemTheme }) => {
|
||||
if (cancelled) return
|
||||
cleanup = watchSystemTheme(internal_querier, setSystemTheme)
|
||||
},
|
||||
)
|
||||
return () => {
|
||||
cancelled = true;
|
||||
cleanup?.();
|
||||
};
|
||||
cancelled = true
|
||||
cleanup?.()
|
||||
}
|
||||
}
|
||||
}, [activeSetting, internal_querier]);
|
||||
const currentTheme: ThemeName = activeSetting === 'auto' ? systemTheme : activeSetting;
|
||||
const value = useMemo<ThemeContextValue>(() => ({
|
||||
themeSetting,
|
||||
setThemeSetting: (newSetting: ThemeSetting) => {
|
||||
setThemeSetting(newSetting);
|
||||
setPreviewTheme(null);
|
||||
// Switching to 'auto' restarts the watcher (activeSetting dep), whose
|
||||
// first poll fires immediately. Seed from the cache so the OSC
|
||||
// round-trip doesn't flash the wrong palette.
|
||||
if (newSetting === 'auto') {
|
||||
setSystemTheme(getSystemThemeName());
|
||||
}
|
||||
onThemeSave?.(newSetting);
|
||||
},
|
||||
setPreviewTheme: (newSetting_0: ThemeSetting) => {
|
||||
setPreviewTheme(newSetting_0);
|
||||
if (newSetting_0 === 'auto') {
|
||||
setSystemTheme(getSystemThemeName());
|
||||
}
|
||||
},
|
||||
savePreview: () => {
|
||||
if (previewTheme !== null) {
|
||||
setThemeSetting(previewTheme);
|
||||
setPreviewTheme(null);
|
||||
onThemeSave?.(previewTheme);
|
||||
}
|
||||
},
|
||||
cancelPreview: () => {
|
||||
if (previewTheme !== null) {
|
||||
setPreviewTheme(null);
|
||||
}
|
||||
},
|
||||
currentTheme
|
||||
}), [themeSetting, previewTheme, currentTheme, onThemeSave]);
|
||||
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
||||
}, [activeSetting, internal_querier])
|
||||
|
||||
const currentTheme: ThemeName =
|
||||
activeSetting === 'auto' ? systemTheme : activeSetting
|
||||
|
||||
const value = useMemo<ThemeContextValue>(
|
||||
() => ({
|
||||
themeSetting,
|
||||
setThemeSetting: (newSetting: ThemeSetting) => {
|
||||
setThemeSetting(newSetting)
|
||||
setPreviewTheme(null)
|
||||
// Switching to 'auto' restarts the watcher (activeSetting dep), whose
|
||||
// first poll fires immediately. Seed from the cache so the OSC
|
||||
// round-trip doesn't flash the wrong palette.
|
||||
if (newSetting === 'auto') {
|
||||
setSystemTheme(getSystemThemeName())
|
||||
}
|
||||
onThemeSave?.(newSetting)
|
||||
},
|
||||
setPreviewTheme: (newSetting: ThemeSetting) => {
|
||||
setPreviewTheme(newSetting)
|
||||
if (newSetting === 'auto') {
|
||||
setSystemTheme(getSystemThemeName())
|
||||
}
|
||||
},
|
||||
savePreview: () => {
|
||||
if (previewTheme !== null) {
|
||||
setThemeSetting(previewTheme)
|
||||
setPreviewTheme(null)
|
||||
onThemeSave?.(previewTheme)
|
||||
}
|
||||
},
|
||||
cancelPreview: () => {
|
||||
if (previewTheme !== null) {
|
||||
setPreviewTheme(null)
|
||||
}
|
||||
},
|
||||
currentTheme,
|
||||
}),
|
||||
[themeSetting, previewTheme, currentTheme, onThemeSave],
|
||||
)
|
||||
|
||||
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resolved theme for rendering (never 'auto') and a setter that
|
||||
* accepts any ThemeSetting (including 'auto').
|
||||
*/
|
||||
export function useTheme() {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
currentTheme,
|
||||
setThemeSetting
|
||||
} = useContext(ThemeContext);
|
||||
let t0;
|
||||
if ($[0] !== currentTheme || $[1] !== setThemeSetting) {
|
||||
t0 = [currentTheme, setThemeSetting];
|
||||
$[0] = currentTheme;
|
||||
$[1] = setThemeSetting;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
return t0;
|
||||
export function useTheme(): [ThemeName, (setting: ThemeSetting) => void] {
|
||||
const { currentTheme, setThemeSetting } = useContext(ThemeContext)
|
||||
return [currentTheme, setThemeSetting]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw theme setting as stored in config. Use this in UI that
|
||||
* needs to show 'auto' as a distinct choice (e.g., ThemePicker).
|
||||
*/
|
||||
export function useThemeSetting() {
|
||||
return useContext(ThemeContext).themeSetting;
|
||||
export function useThemeSetting(): ThemeSetting {
|
||||
return useContext(ThemeContext).themeSetting
|
||||
}
|
||||
|
||||
export function usePreviewTheme() {
|
||||
const $ = _c(4);
|
||||
const {
|
||||
setPreviewTheme,
|
||||
savePreview,
|
||||
cancelPreview
|
||||
} = useContext(ThemeContext);
|
||||
let t0;
|
||||
if ($[0] !== cancelPreview || $[1] !== savePreview || $[2] !== setPreviewTheme) {
|
||||
t0 = {
|
||||
setPreviewTheme,
|
||||
savePreview,
|
||||
cancelPreview
|
||||
};
|
||||
$[0] = cancelPreview;
|
||||
$[1] = savePreview;
|
||||
$[2] = setPreviewTheme;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
}
|
||||
return t0;
|
||||
const { setPreviewTheme, savePreview, cancelPreview } =
|
||||
useContext(ThemeContext)
|
||||
return { setPreviewTheme, savePreview, cancelPreview }
|
||||
}
|
||||
|
||||
@@ -1,155 +1,112 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { type PropsWithChildren, type Ref } from 'react';
|
||||
import Box from '../../ink/components/Box.js';
|
||||
import type { DOMElement } from '../../ink/dom.js';
|
||||
import type { ClickEvent } from '../../ink/events/click-event.js';
|
||||
import type { FocusEvent } from '../../ink/events/focus-event.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import type { Color, Styles } from '../../ink/styles.js';
|
||||
import { getTheme, type Theme } from '../../utils/theme.js';
|
||||
import { useTheme } from './ThemeProvider.js';
|
||||
import React, { type PropsWithChildren, type Ref } from 'react'
|
||||
import Box from '../../ink/components/Box.js'
|
||||
import type { DOMElement } from '../../ink/dom.js'
|
||||
import type { ClickEvent } from '../../ink/events/click-event.js'
|
||||
import type { FocusEvent } from '../../ink/events/focus-event.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import type { Color, Styles } from '../../ink/styles.js'
|
||||
import { getTheme, type Theme } from '../../utils/theme.js'
|
||||
import { useTheme } from './ThemeProvider.js'
|
||||
|
||||
// Color props that accept theme keys
|
||||
type ThemedColorProps = {
|
||||
readonly borderColor?: keyof Theme | Color;
|
||||
readonly borderTopColor?: keyof Theme | Color;
|
||||
readonly borderBottomColor?: keyof Theme | Color;
|
||||
readonly borderLeftColor?: keyof Theme | Color;
|
||||
readonly borderRightColor?: keyof Theme | Color;
|
||||
readonly backgroundColor?: keyof Theme | Color;
|
||||
};
|
||||
readonly borderColor?: keyof Theme | Color
|
||||
readonly borderTopColor?: keyof Theme | Color
|
||||
readonly borderBottomColor?: keyof Theme | Color
|
||||
readonly borderLeftColor?: keyof Theme | Color
|
||||
readonly borderRightColor?: keyof Theme | Color
|
||||
readonly backgroundColor?: keyof Theme | Color
|
||||
}
|
||||
|
||||
// Base Styles without color props (they'll be overridden)
|
||||
type BaseStylesWithoutColors = Omit<Styles, 'textWrap' | 'borderColor' | 'borderTopColor' | 'borderBottomColor' | 'borderLeftColor' | 'borderRightColor' | 'backgroundColor'>;
|
||||
export type Props = BaseStylesWithoutColors & ThemedColorProps & {
|
||||
ref?: Ref<DOMElement>;
|
||||
tabIndex?: number;
|
||||
autoFocus?: boolean;
|
||||
onClick?: (event: ClickEvent) => void;
|
||||
onFocus?: (event: FocusEvent) => void;
|
||||
onFocusCapture?: (event: FocusEvent) => void;
|
||||
onBlur?: (event: FocusEvent) => void;
|
||||
onBlurCapture?: (event: FocusEvent) => void;
|
||||
onKeyDown?: (event: KeyboardEvent) => void;
|
||||
onKeyDownCapture?: (event: KeyboardEvent) => void;
|
||||
onMouseEnter?: () => void;
|
||||
onMouseLeave?: () => void;
|
||||
};
|
||||
type BaseStylesWithoutColors = Omit<
|
||||
Styles,
|
||||
| 'textWrap'
|
||||
| 'borderColor'
|
||||
| 'borderTopColor'
|
||||
| 'borderBottomColor'
|
||||
| 'borderLeftColor'
|
||||
| 'borderRightColor'
|
||||
| 'backgroundColor'
|
||||
>
|
||||
|
||||
export type Props = BaseStylesWithoutColors &
|
||||
ThemedColorProps & {
|
||||
ref?: Ref<DOMElement>
|
||||
tabIndex?: number
|
||||
autoFocus?: boolean
|
||||
onClick?: (event: ClickEvent) => void
|
||||
onFocus?: (event: FocusEvent) => void
|
||||
onFocusCapture?: (event: FocusEvent) => void
|
||||
onBlur?: (event: FocusEvent) => void
|
||||
onBlurCapture?: (event: FocusEvent) => void
|
||||
onKeyDown?: (event: KeyboardEvent) => void
|
||||
onKeyDownCapture?: (event: KeyboardEvent) => void
|
||||
onMouseEnter?: () => void
|
||||
onMouseLeave?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a color value that may be a theme key to a raw Color.
|
||||
*/
|
||||
function resolveColor(color: keyof Theme | Color | undefined, theme: Theme): Color | undefined {
|
||||
if (!color) return undefined;
|
||||
function resolveColor(
|
||||
color: keyof Theme | Color | undefined,
|
||||
theme: Theme,
|
||||
): Color | undefined {
|
||||
if (!color) return undefined
|
||||
// Check if it's a raw color (starts with rgb(, #, ansi256(, or ansi:)
|
||||
if (color.startsWith('rgb(') || color.startsWith('#') || color.startsWith('ansi256(') || color.startsWith('ansi:')) {
|
||||
return color as Color;
|
||||
if (
|
||||
color.startsWith('rgb(') ||
|
||||
color.startsWith('#') ||
|
||||
color.startsWith('ansi256(') ||
|
||||
color.startsWith('ansi:')
|
||||
) {
|
||||
return color as Color
|
||||
}
|
||||
// It's a theme key - resolve it
|
||||
return theme[color as keyof Theme] as Color;
|
||||
return theme[color as keyof Theme] as Color
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme-aware Box component that resolves theme color keys to raw colors.
|
||||
* This wraps the base Box component with theme resolution for border colors.
|
||||
*/
|
||||
function ThemedBox(t0) {
|
||||
const $ = _c(33);
|
||||
let backgroundColor;
|
||||
let borderBottomColor;
|
||||
let borderColor;
|
||||
let borderLeftColor;
|
||||
let borderRightColor;
|
||||
let borderTopColor;
|
||||
let children;
|
||||
let ref;
|
||||
let rest;
|
||||
if ($[0] !== t0) {
|
||||
({
|
||||
borderColor,
|
||||
borderTopColor,
|
||||
borderBottomColor,
|
||||
borderLeftColor,
|
||||
borderRightColor,
|
||||
backgroundColor,
|
||||
children,
|
||||
ref,
|
||||
...rest
|
||||
} = t0);
|
||||
$[0] = t0;
|
||||
$[1] = backgroundColor;
|
||||
$[2] = borderBottomColor;
|
||||
$[3] = borderColor;
|
||||
$[4] = borderLeftColor;
|
||||
$[5] = borderRightColor;
|
||||
$[6] = borderTopColor;
|
||||
$[7] = children;
|
||||
$[8] = ref;
|
||||
$[9] = rest;
|
||||
} else {
|
||||
backgroundColor = $[1];
|
||||
borderBottomColor = $[2];
|
||||
borderColor = $[3];
|
||||
borderLeftColor = $[4];
|
||||
borderRightColor = $[5];
|
||||
borderTopColor = $[6];
|
||||
children = $[7];
|
||||
ref = $[8];
|
||||
rest = $[9];
|
||||
}
|
||||
const [themeName] = useTheme();
|
||||
let resolvedBorderBottomColor;
|
||||
let resolvedBorderColor;
|
||||
let resolvedBorderLeftColor;
|
||||
let resolvedBorderRightColor;
|
||||
let resolvedBorderTopColor;
|
||||
let t1;
|
||||
if ($[10] !== backgroundColor || $[11] !== borderBottomColor || $[12] !== borderColor || $[13] !== borderLeftColor || $[14] !== borderRightColor || $[15] !== borderTopColor || $[16] !== themeName) {
|
||||
const theme = getTheme(themeName);
|
||||
resolvedBorderColor = resolveColor(borderColor, theme);
|
||||
resolvedBorderTopColor = resolveColor(borderTopColor, theme);
|
||||
resolvedBorderBottomColor = resolveColor(borderBottomColor, theme);
|
||||
resolvedBorderLeftColor = resolveColor(borderLeftColor, theme);
|
||||
resolvedBorderRightColor = resolveColor(borderRightColor, theme);
|
||||
t1 = resolveColor(backgroundColor, theme);
|
||||
$[10] = backgroundColor;
|
||||
$[11] = borderBottomColor;
|
||||
$[12] = borderColor;
|
||||
$[13] = borderLeftColor;
|
||||
$[14] = borderRightColor;
|
||||
$[15] = borderTopColor;
|
||||
$[16] = themeName;
|
||||
$[17] = resolvedBorderBottomColor;
|
||||
$[18] = resolvedBorderColor;
|
||||
$[19] = resolvedBorderLeftColor;
|
||||
$[20] = resolvedBorderRightColor;
|
||||
$[21] = resolvedBorderTopColor;
|
||||
$[22] = t1;
|
||||
} else {
|
||||
resolvedBorderBottomColor = $[17];
|
||||
resolvedBorderColor = $[18];
|
||||
resolvedBorderLeftColor = $[19];
|
||||
resolvedBorderRightColor = $[20];
|
||||
resolvedBorderTopColor = $[21];
|
||||
t1 = $[22];
|
||||
}
|
||||
const resolvedBackgroundColor = t1;
|
||||
let t2;
|
||||
if ($[23] !== children || $[24] !== ref || $[25] !== resolvedBackgroundColor || $[26] !== resolvedBorderBottomColor || $[27] !== resolvedBorderColor || $[28] !== resolvedBorderLeftColor || $[29] !== resolvedBorderRightColor || $[30] !== resolvedBorderTopColor || $[31] !== rest) {
|
||||
t2 = <Box ref={ref} borderColor={resolvedBorderColor} borderTopColor={resolvedBorderTopColor} borderBottomColor={resolvedBorderBottomColor} borderLeftColor={resolvedBorderLeftColor} borderRightColor={resolvedBorderRightColor} backgroundColor={resolvedBackgroundColor} {...rest}>{children}</Box>;
|
||||
$[23] = children;
|
||||
$[24] = ref;
|
||||
$[25] = resolvedBackgroundColor;
|
||||
$[26] = resolvedBorderBottomColor;
|
||||
$[27] = resolvedBorderColor;
|
||||
$[28] = resolvedBorderLeftColor;
|
||||
$[29] = resolvedBorderRightColor;
|
||||
$[30] = resolvedBorderTopColor;
|
||||
$[31] = rest;
|
||||
$[32] = t2;
|
||||
} else {
|
||||
t2 = $[32];
|
||||
}
|
||||
return t2;
|
||||
function ThemedBox({
|
||||
borderColor,
|
||||
borderTopColor,
|
||||
borderBottomColor,
|
||||
borderLeftColor,
|
||||
borderRightColor,
|
||||
backgroundColor,
|
||||
children,
|
||||
ref,
|
||||
...rest
|
||||
}: PropsWithChildren<Props>): React.ReactNode {
|
||||
const [themeName] = useTheme()
|
||||
const theme = getTheme(themeName)
|
||||
|
||||
// Resolve theme keys to raw colors
|
||||
const resolvedBorderColor = resolveColor(borderColor, theme)
|
||||
const resolvedBorderTopColor = resolveColor(borderTopColor, theme)
|
||||
const resolvedBorderBottomColor = resolveColor(borderBottomColor, theme)
|
||||
const resolvedBorderLeftColor = resolveColor(borderLeftColor, theme)
|
||||
const resolvedBorderRightColor = resolveColor(borderRightColor, theme)
|
||||
const resolvedBackgroundColor = resolveColor(backgroundColor, theme)
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
borderColor={resolvedBorderColor}
|
||||
borderTopColor={resolvedBorderTopColor}
|
||||
borderBottomColor={resolvedBorderBottomColor}
|
||||
borderLeftColor={resolvedBorderLeftColor}
|
||||
borderRightColor={resolvedBorderRightColor}
|
||||
backgroundColor={resolvedBackgroundColor}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
export default ThemedBox;
|
||||
|
||||
export default ThemedBox
|
||||
|
||||
@@ -1,123 +1,132 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import Text from '../../ink/components/Text.js';
|
||||
import type { Color, Styles } from '../../ink/styles.js';
|
||||
import { getTheme, type Theme } from '../../utils/theme.js';
|
||||
import { useTheme } from './ThemeProvider.js';
|
||||
import type { ReactNode } from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import Text from '../../ink/components/Text.js'
|
||||
import type { Color, Styles } from '../../ink/styles.js'
|
||||
import { getTheme, type Theme } from '../../utils/theme.js'
|
||||
import { useTheme } from './ThemeProvider.js'
|
||||
|
||||
/** Colors uncolored ThemedText in the subtree. Precedence: explicit `color` >
|
||||
* this > dimColor. Crosses Box boundaries (Ink's style cascade doesn't). */
|
||||
export const TextHoverColorContext = React.createContext<keyof Theme | undefined>(undefined);
|
||||
export const TextHoverColorContext = React.createContext<
|
||||
keyof Theme | undefined
|
||||
>(undefined)
|
||||
|
||||
export type Props = {
|
||||
/**
|
||||
* Change text color. Accepts a theme key or raw color value.
|
||||
*/
|
||||
readonly color?: keyof Theme | Color;
|
||||
readonly color?: keyof Theme | Color
|
||||
|
||||
/**
|
||||
* Same as `color`, but for background. Must be a theme key.
|
||||
*/
|
||||
readonly backgroundColor?: keyof Theme;
|
||||
readonly backgroundColor?: keyof Theme
|
||||
|
||||
/**
|
||||
* Dim the color using the theme's inactive color.
|
||||
* This is compatible with bold (unlike ANSI dim).
|
||||
*/
|
||||
readonly dimColor?: boolean;
|
||||
readonly dimColor?: boolean
|
||||
|
||||
/**
|
||||
* Make the text bold.
|
||||
*/
|
||||
readonly bold?: boolean;
|
||||
readonly bold?: boolean
|
||||
|
||||
/**
|
||||
* Make the text italic.
|
||||
*/
|
||||
readonly italic?: boolean;
|
||||
readonly italic?: boolean
|
||||
|
||||
/**
|
||||
* Make the text underlined.
|
||||
*/
|
||||
readonly underline?: boolean;
|
||||
readonly underline?: boolean
|
||||
|
||||
/**
|
||||
* Make the text crossed with a line.
|
||||
*/
|
||||
readonly strikethrough?: boolean;
|
||||
readonly strikethrough?: boolean
|
||||
|
||||
/**
|
||||
* Inverse background and foreground colors.
|
||||
*/
|
||||
readonly inverse?: boolean;
|
||||
readonly inverse?: boolean
|
||||
|
||||
/**
|
||||
* This property tells Ink to wrap or truncate text if its width is larger than container.
|
||||
* If `wrap` is passed (by default), Ink will wrap text and split it into multiple lines.
|
||||
* If `truncate-*` is passed, Ink will truncate text instead, which will result in one line of text with the rest cut off.
|
||||
*/
|
||||
readonly wrap?: Styles['textWrap'];
|
||||
readonly children?: ReactNode;
|
||||
};
|
||||
readonly wrap?: Styles['textWrap']
|
||||
|
||||
readonly children?: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a color value that may be a theme key to a raw Color.
|
||||
*/
|
||||
function resolveColor(color: keyof Theme | Color | undefined, theme: Theme): Color | undefined {
|
||||
if (!color) return undefined;
|
||||
function resolveColor(
|
||||
color: keyof Theme | Color | undefined,
|
||||
theme: Theme,
|
||||
): Color | undefined {
|
||||
if (!color) return undefined
|
||||
// Check if it's a raw color (starts with rgb(, #, ansi256(, or ansi:)
|
||||
if (color.startsWith('rgb(') || color.startsWith('#') || color.startsWith('ansi256(') || color.startsWith('ansi:')) {
|
||||
return color as Color;
|
||||
if (
|
||||
color.startsWith('rgb(') ||
|
||||
color.startsWith('#') ||
|
||||
color.startsWith('ansi256(') ||
|
||||
color.startsWith('ansi:')
|
||||
) {
|
||||
return color as Color
|
||||
}
|
||||
// It's a theme key - resolve it
|
||||
return theme[color as keyof Theme] as Color;
|
||||
return theme[color as keyof Theme] as Color
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme-aware Text component that resolves theme color keys to raw colors.
|
||||
* This wraps the base Text component with theme resolution.
|
||||
*/
|
||||
export default function ThemedText(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
color,
|
||||
backgroundColor,
|
||||
dimColor: t1,
|
||||
bold: t2,
|
||||
italic: t3,
|
||||
underline: t4,
|
||||
strikethrough: t5,
|
||||
inverse: t6,
|
||||
wrap: t7,
|
||||
children
|
||||
} = t0;
|
||||
const dimColor = t1 === undefined ? false : t1;
|
||||
const bold = t2 === undefined ? false : t2;
|
||||
const italic = t3 === undefined ? false : t3;
|
||||
const underline = t4 === undefined ? false : t4;
|
||||
const strikethrough = t5 === undefined ? false : t5;
|
||||
const inverse = t6 === undefined ? false : t6;
|
||||
const wrap = t7 === undefined ? "wrap" : t7;
|
||||
const [themeName] = useTheme();
|
||||
const theme = getTheme(themeName);
|
||||
const hoverColor = useContext(TextHoverColorContext);
|
||||
const resolvedColor = !color && hoverColor ? resolveColor(hoverColor, theme) : dimColor ? theme.inactive as Color : resolveColor(color, theme);
|
||||
const resolvedBackgroundColor = backgroundColor ? theme[backgroundColor] as Color : undefined;
|
||||
let t8;
|
||||
if ($[0] !== bold || $[1] !== children || $[2] !== inverse || $[3] !== italic || $[4] !== resolvedBackgroundColor || $[5] !== resolvedColor || $[6] !== strikethrough || $[7] !== underline || $[8] !== wrap) {
|
||||
t8 = <Text color={resolvedColor} backgroundColor={resolvedBackgroundColor} bold={bold} italic={italic} underline={underline} strikethrough={strikethrough} inverse={inverse} wrap={wrap}>{children}</Text>;
|
||||
$[0] = bold;
|
||||
$[1] = children;
|
||||
$[2] = inverse;
|
||||
$[3] = italic;
|
||||
$[4] = resolvedBackgroundColor;
|
||||
$[5] = resolvedColor;
|
||||
$[6] = strikethrough;
|
||||
$[7] = underline;
|
||||
$[8] = wrap;
|
||||
$[9] = t8;
|
||||
} else {
|
||||
t8 = $[9];
|
||||
}
|
||||
return t8;
|
||||
export default function ThemedText({
|
||||
color,
|
||||
backgroundColor,
|
||||
dimColor = false,
|
||||
bold = false,
|
||||
italic = false,
|
||||
underline = false,
|
||||
strikethrough = false,
|
||||
inverse = false,
|
||||
wrap = 'wrap',
|
||||
children,
|
||||
}: Props): React.ReactNode {
|
||||
const [themeName] = useTheme()
|
||||
const theme = getTheme(themeName)
|
||||
const hoverColor = useContext(TextHoverColorContext)
|
||||
|
||||
// Resolve theme keys to raw colors
|
||||
const resolvedColor =
|
||||
!color && hoverColor
|
||||
? resolveColor(hoverColor, theme)
|
||||
: dimColor
|
||||
? (theme.inactive as Color)
|
||||
: resolveColor(color, theme)
|
||||
const resolvedBackgroundColor = backgroundColor
|
||||
? (theme[backgroundColor] as Color)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Text
|
||||
color={resolvedColor}
|
||||
backgroundColor={resolvedBackgroundColor}
|
||||
bold={bold}
|
||||
italic={italic}
|
||||
underline={underline}
|
||||
strikethrough={strikethrough}
|
||||
inverse={inverse}
|
||||
wrap={wrap}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,236 +1,205 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import capitalize from 'lodash-es/capitalize.js';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { type Command, type CommandBase, type CommandResultDisplay, getCommandName, type PromptCommand } from '../../commands.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { estimateSkillFrontmatterTokens, getSkillsPath } from '../../skills/loadSkillsDir.js';
|
||||
import { getDisplayPath } from '../../utils/file.js';
|
||||
import { formatTokens } from '../../utils/format.js';
|
||||
import { getSettingSourceName, type SettingSource } from '../../utils/settings/constants.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
||||
import { Dialog } from '../design-system/Dialog.js';
|
||||
import capitalize from 'lodash-es/capitalize.js'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
type Command,
|
||||
type CommandBase,
|
||||
type CommandResultDisplay,
|
||||
getCommandName,
|
||||
type PromptCommand,
|
||||
} from '../../commands.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import {
|
||||
estimateSkillFrontmatterTokens,
|
||||
getSkillsPath,
|
||||
} from '../../skills/loadSkillsDir.js'
|
||||
import { getDisplayPath } from '../../utils/file.js'
|
||||
import { formatTokens } from '../../utils/format.js'
|
||||
import {
|
||||
getSettingSourceName,
|
||||
type SettingSource,
|
||||
} from '../../utils/settings/constants.js'
|
||||
import { plural } from '../../utils/stringUtils.js'
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||
import { Dialog } from '../design-system/Dialog.js'
|
||||
|
||||
// Skills are always PromptCommands with CommandBase properties
|
||||
type SkillCommand = CommandBase & PromptCommand;
|
||||
type SkillSource = SettingSource | 'plugin' | 'mcp';
|
||||
type SkillCommand = CommandBase & PromptCommand
|
||||
|
||||
type SkillSource = SettingSource | 'plugin' | 'mcp'
|
||||
|
||||
type Props = {
|
||||
onExit: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
commands: Command[];
|
||||
};
|
||||
onExit: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
commands: Command[]
|
||||
}
|
||||
|
||||
function getSourceTitle(source: SkillSource): string {
|
||||
if (source === 'plugin') {
|
||||
return 'Plugin skills';
|
||||
return 'Plugin skills'
|
||||
}
|
||||
if (source === 'mcp') {
|
||||
return 'MCP skills';
|
||||
return 'MCP skills'
|
||||
}
|
||||
return `${capitalize(getSettingSourceName(source))} skills`;
|
||||
return `${capitalize(getSettingSourceName(source))} skills`
|
||||
}
|
||||
function getSourceSubtitle(source: SkillSource, skills: SkillCommand[]): string | undefined {
|
||||
|
||||
function getSourceSubtitle(
|
||||
source: SkillSource,
|
||||
skills: SkillCommand[],
|
||||
): string | undefined {
|
||||
// MCP skills show server names; file-based skills show filesystem paths.
|
||||
// Skill names are `<server>:<skill>`, not `mcp__<server>__…`.
|
||||
if (source === 'mcp') {
|
||||
const servers = [...new Set(skills.map(s => {
|
||||
const idx = s.name.indexOf(':');
|
||||
return idx > 0 ? s.name.slice(0, idx) : null;
|
||||
}).filter((n): n is string => n != null))];
|
||||
return servers.length > 0 ? servers.join(', ') : undefined;
|
||||
const servers = [
|
||||
...new Set(
|
||||
skills
|
||||
.map(s => {
|
||||
const idx = s.name.indexOf(':')
|
||||
return idx > 0 ? s.name.slice(0, idx) : null
|
||||
})
|
||||
.filter((n): n is string => n != null),
|
||||
),
|
||||
]
|
||||
return servers.length > 0 ? servers.join(', ') : undefined
|
||||
}
|
||||
const skillsPath = getDisplayPath(getSkillsPath(source, 'skills'));
|
||||
const hasCommandsSkills = skills.some(s => s.loadedFrom === 'commands_DEPRECATED');
|
||||
return hasCommandsSkills ? `${skillsPath}, ${getDisplayPath(getSkillsPath(source, 'commands'))}` : skillsPath;
|
||||
const skillsPath = getDisplayPath(getSkillsPath(source, 'skills'))
|
||||
const hasCommandsSkills = skills.some(
|
||||
s => s.loadedFrom === 'commands_DEPRECATED',
|
||||
)
|
||||
return hasCommandsSkills
|
||||
? `${skillsPath}, ${getDisplayPath(getSkillsPath(source, 'commands'))}`
|
||||
: skillsPath
|
||||
}
|
||||
export function SkillsMenu(t0) {
|
||||
const $ = _c(35);
|
||||
const {
|
||||
onExit,
|
||||
commands
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== commands) {
|
||||
t1 = commands.filter(_temp);
|
||||
$[0] = commands;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const skills = t1;
|
||||
let groups;
|
||||
if ($[2] !== skills) {
|
||||
groups = {
|
||||
|
||||
export function SkillsMenu({ onExit, commands }: Props): React.ReactNode {
|
||||
// Filter commands for skills and cast to SkillCommand
|
||||
const skills = useMemo(() => {
|
||||
return commands.filter(
|
||||
(cmd): cmd is SkillCommand =>
|
||||
cmd.type === 'prompt' &&
|
||||
(cmd.loadedFrom === 'skills' ||
|
||||
cmd.loadedFrom === 'commands_DEPRECATED' ||
|
||||
cmd.loadedFrom === 'plugin' ||
|
||||
cmd.loadedFrom === 'mcp'),
|
||||
)
|
||||
}, [commands])
|
||||
|
||||
const skillsBySource = useMemo((): Record<SkillSource, SkillCommand[]> => {
|
||||
const groups: Record<SkillSource, SkillCommand[]> = {
|
||||
policySettings: [],
|
||||
userSettings: [],
|
||||
projectSettings: [],
|
||||
localSettings: [],
|
||||
flagSettings: [],
|
||||
plugin: [],
|
||||
mcp: []
|
||||
};
|
||||
mcp: [],
|
||||
}
|
||||
|
||||
for (const skill of skills) {
|
||||
const source = skill.source as SkillSource;
|
||||
const source = skill.source as SkillSource
|
||||
if (source in groups) {
|
||||
groups[source].push(skill);
|
||||
groups[source].push(skill)
|
||||
}
|
||||
}
|
||||
|
||||
for (const group of Object.values(groups)) {
|
||||
(group as Array<{ name: string }>).sort(_temp2);
|
||||
group.sort((a, b) => getCommandName(a).localeCompare(getCommandName(b)))
|
||||
}
|
||||
$[2] = skills;
|
||||
$[3] = groups;
|
||||
} else {
|
||||
groups = $[3];
|
||||
|
||||
return groups
|
||||
}, [skills])
|
||||
|
||||
const handleCancel = (): void => {
|
||||
onExit('Skills dialog dismissed', { display: 'system' })
|
||||
}
|
||||
const skillsBySource = groups;
|
||||
let t2;
|
||||
if ($[4] !== onExit) {
|
||||
t2 = () => {
|
||||
onExit("Skills dialog dismissed", {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[4] = onExit;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const handleCancel = t2;
|
||||
|
||||
if (skills.length === 0) {
|
||||
let t3;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Text dimColor={true}>Create skills in .claude/skills/ or ~/.claude/skills/</Text>;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text>;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] !== handleCancel) {
|
||||
t5 = <Dialog title="Skills" subtitle="No skills found" onCancel={handleCancel} hideInputGuide={true}>{t3}{t4}</Dialog>;
|
||||
$[8] = handleCancel;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
return t5;
|
||||
return (
|
||||
<Dialog
|
||||
title="Skills"
|
||||
subtitle="No skills found"
|
||||
onCancel={handleCancel}
|
||||
hideInputGuide
|
||||
>
|
||||
<Text dimColor>
|
||||
Create skills in .claude/skills/ or ~/.claude/skills/
|
||||
</Text>
|
||||
<Text dimColor italic>
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="close"
|
||||
/>
|
||||
</Text>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
const renderSkill = _temp3;
|
||||
let t3;
|
||||
if ($[10] !== skillsBySource) {
|
||||
t3 = source_0 => {
|
||||
const groupSkills = skillsBySource[source_0];
|
||||
if (groupSkills.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const title = getSourceTitle(source_0);
|
||||
const subtitle = getSourceSubtitle(source_0, groupSkills);
|
||||
return <Box flexDirection="column" key={source_0}><Box><Text bold={true} dimColor={true}>{title}</Text>{subtitle && <Text dimColor={true}> ({subtitle})</Text>}</Box>{groupSkills.map(skill_1 => renderSkill(skill_1))}</Box>;
|
||||
};
|
||||
$[10] = skillsBySource;
|
||||
$[11] = t3;
|
||||
} else {
|
||||
t3 = $[11];
|
||||
|
||||
const renderSkill = (skill: SkillCommand) => {
|
||||
const estimatedTokens = estimateSkillFrontmatterTokens(skill)
|
||||
const tokenDisplay = `~${formatTokens(estimatedTokens)}`
|
||||
const pluginName =
|
||||
skill.source === 'plugin'
|
||||
? skill.pluginInfo?.pluginManifest.name
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Box key={`${skill.name}-${skill.source}`}>
|
||||
<Text>{getCommandName(skill)}</Text>
|
||||
<Text dimColor>
|
||||
{pluginName ? ` · ${pluginName}` : ''} · {tokenDisplay} description
|
||||
tokens
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
const renderSkillGroup = t3;
|
||||
const t4 = skills.length;
|
||||
let t5;
|
||||
if ($[12] !== skills.length) {
|
||||
t5 = plural(skills.length, "skill");
|
||||
$[12] = skills.length;
|
||||
$[13] = t5;
|
||||
} else {
|
||||
t5 = $[13];
|
||||
|
||||
const renderSkillGroup = (source: SkillSource) => {
|
||||
const groupSkills = skillsBySource[source]
|
||||
if (groupSkills.length === 0) return null
|
||||
|
||||
const title = getSourceTitle(source)
|
||||
const subtitle = getSourceSubtitle(source, groupSkills)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" key={source}>
|
||||
<Box>
|
||||
<Text bold dimColor>
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle && <Text dimColor> ({subtitle})</Text>}
|
||||
</Box>
|
||||
{groupSkills.map(skill => renderSkill(skill))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
const t6 = `${t4} ${t5}`;
|
||||
let t7;
|
||||
if ($[14] !== renderSkillGroup) {
|
||||
t7 = renderSkillGroup("projectSettings");
|
||||
$[14] = renderSkillGroup;
|
||||
$[15] = t7;
|
||||
} else {
|
||||
t7 = $[15];
|
||||
}
|
||||
let t8;
|
||||
if ($[16] !== renderSkillGroup) {
|
||||
t8 = renderSkillGroup("userSettings");
|
||||
$[16] = renderSkillGroup;
|
||||
$[17] = t8;
|
||||
} else {
|
||||
t8 = $[17];
|
||||
}
|
||||
let t9;
|
||||
if ($[18] !== renderSkillGroup) {
|
||||
t9 = renderSkillGroup("policySettings");
|
||||
$[18] = renderSkillGroup;
|
||||
$[19] = t9;
|
||||
} else {
|
||||
t9 = $[19];
|
||||
}
|
||||
let t10;
|
||||
if ($[20] !== renderSkillGroup) {
|
||||
t10 = renderSkillGroup("plugin");
|
||||
$[20] = renderSkillGroup;
|
||||
$[21] = t10;
|
||||
} else {
|
||||
t10 = $[21];
|
||||
}
|
||||
let t11;
|
||||
if ($[22] !== renderSkillGroup) {
|
||||
t11 = renderSkillGroup("mcp");
|
||||
$[22] = renderSkillGroup;
|
||||
$[23] = t11;
|
||||
} else {
|
||||
t11 = $[23];
|
||||
}
|
||||
let t12;
|
||||
if ($[24] !== t10 || $[25] !== t11 || $[26] !== t7 || $[27] !== t8 || $[28] !== t9) {
|
||||
t12 = <Box flexDirection="column" gap={1}>{t7}{t8}{t9}{t10}{t11}</Box>;
|
||||
$[24] = t10;
|
||||
$[25] = t11;
|
||||
$[26] = t7;
|
||||
$[27] = t8;
|
||||
$[28] = t9;
|
||||
$[29] = t12;
|
||||
} else {
|
||||
t12 = $[29];
|
||||
}
|
||||
let t13;
|
||||
if ($[30] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t13 = <Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text>;
|
||||
$[30] = t13;
|
||||
} else {
|
||||
t13 = $[30];
|
||||
}
|
||||
let t14;
|
||||
if ($[31] !== handleCancel || $[32] !== t12 || $[33] !== t6) {
|
||||
t14 = <Dialog title="Skills" subtitle={t6} onCancel={handleCancel} hideInputGuide={true}>{t12}{t13}</Dialog>;
|
||||
$[31] = handleCancel;
|
||||
$[32] = t12;
|
||||
$[33] = t6;
|
||||
$[34] = t14;
|
||||
} else {
|
||||
t14 = $[34];
|
||||
}
|
||||
return t14;
|
||||
}
|
||||
function _temp3(skill_0) {
|
||||
const estimatedTokens = estimateSkillFrontmatterTokens(skill_0);
|
||||
const tokenDisplay = `~${formatTokens(estimatedTokens)}`;
|
||||
const pluginName = skill_0.source === "plugin" ? skill_0.pluginInfo?.pluginManifest.name : undefined;
|
||||
return <Box key={`${skill_0.name}-${skill_0.source}`}><Text>{getCommandName(skill_0)}</Text><Text dimColor={true}>{pluginName ? ` · ${pluginName}` : ""} · {tokenDisplay} description tokens</Text></Box>;
|
||||
}
|
||||
function _temp2(a, b) {
|
||||
return getCommandName(a).localeCompare(getCommandName(b));
|
||||
}
|
||||
function _temp(cmd) {
|
||||
return cmd.type === "prompt" && (cmd.loadedFrom === "skills" || cmd.loadedFrom === "commands_DEPRECATED" || cmd.loadedFrom === "plugin" || cmd.loadedFrom === "mcp");
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Skills"
|
||||
subtitle={`${skills.length} ${plural(skills.length, 'skill')}`}
|
||||
onCancel={handleCancel}
|
||||
hideInputGuide
|
||||
>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
{renderSkillGroup('projectSettings')}
|
||||
{renderSkillGroup('userSettings')}
|
||||
{renderSkillGroup('policySettings')}
|
||||
{renderSkillGroup('plugin')}
|
||||
{renderSkillGroup('mcp')}
|
||||
</Box>
|
||||
<Text dimColor italic>
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="close"
|
||||
/>
|
||||
</Text>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,228 +1,200 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useMemo } from 'react';
|
||||
import type { DeepImmutable } from 'src/types/utils.js';
|
||||
import { useElapsedTime } from '../../hooks/useElapsedTime.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { Box, Text, useTheme } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import { getEmptyToolPermissionContext } from '../../Tool.js';
|
||||
import type { LocalAgentTaskState } from '../../tasks/LocalAgentTask/LocalAgentTask.js';
|
||||
import { getTools } from '../../tools.js';
|
||||
import { formatNumber } from '../../utils/format.js';
|
||||
import { extractTag } from '../../utils/messages.js';
|
||||
import { Byline } from '../design-system/Byline.js';
|
||||
import { Dialog } from '../design-system/Dialog.js';
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
||||
import { UserPlanMessage } from '../messages/UserPlanMessage.js';
|
||||
import { renderToolActivity } from './renderToolActivity.js';
|
||||
import { getTaskStatusColor, getTaskStatusIcon } from './taskStatusUtils.js';
|
||||
import React, { useMemo } from 'react'
|
||||
import type { DeepImmutable } from 'src/types/utils.js'
|
||||
import { useElapsedTime } from '../../hooks/useElapsedTime.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { Box, Text, useTheme } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import { getEmptyToolPermissionContext } from '../../Tool.js'
|
||||
import type { LocalAgentTaskState } from '../../tasks/LocalAgentTask/LocalAgentTask.js'
|
||||
import { getTools } from '../../tools.js'
|
||||
import { formatNumber } from '../../utils/format.js'
|
||||
import { extractTag } from '../../utils/messages.js'
|
||||
import { Byline } from '../design-system/Byline.js'
|
||||
import { Dialog } from '../design-system/Dialog.js'
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||
import { UserPlanMessage } from '../messages/UserPlanMessage.js'
|
||||
import { renderToolActivity } from './renderToolActivity.js'
|
||||
import { getTaskStatusColor, getTaskStatusIcon } from './taskStatusUtils.js'
|
||||
|
||||
type Props = {
|
||||
agent: DeepImmutable<LocalAgentTaskState>;
|
||||
onDone: () => void;
|
||||
onKillAgent?: () => void;
|
||||
onBack?: () => void;
|
||||
};
|
||||
export function AsyncAgentDetailDialog(t0) {
|
||||
const $ = _c(54);
|
||||
const {
|
||||
agent,
|
||||
onDone,
|
||||
onKillAgent,
|
||||
onBack
|
||||
} = t0;
|
||||
const [theme] = useTheme();
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = getTools(getEmptyToolPermissionContext());
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const tools = t1;
|
||||
const elapsedTime = useElapsedTime(agent.startTime, agent.status === "running", 1000, agent.totalPausedMs ?? 0);
|
||||
let t2;
|
||||
if ($[1] !== onDone) {
|
||||
t2 = {
|
||||
"confirm:yes": onDone
|
||||
};
|
||||
$[1] = onDone;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
useKeybindings(t2, t3);
|
||||
let t4;
|
||||
if ($[4] !== agent.status || $[5] !== onBack || $[6] !== onDone || $[7] !== onKillAgent) {
|
||||
t4 = e => {
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
onDone();
|
||||
} else {
|
||||
if (e.key === "left" && onBack) {
|
||||
e.preventDefault();
|
||||
onBack();
|
||||
} else {
|
||||
if (e.key === "x" && agent.status === "running" && onKillAgent) {
|
||||
e.preventDefault();
|
||||
onKillAgent();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
$[4] = agent.status;
|
||||
$[5] = onBack;
|
||||
$[6] = onDone;
|
||||
$[7] = onKillAgent;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
const handleKeyDown = t4;
|
||||
let t5;
|
||||
if ($[9] !== agent.prompt) {
|
||||
t5 = extractTag(agent.prompt, "plan");
|
||||
$[9] = agent.prompt;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
const planContent = t5;
|
||||
const displayPrompt = agent.prompt.length > 300 ? agent.prompt.substring(0, 297) + "\u2026" : agent.prompt;
|
||||
const tokenCount = agent.result?.totalTokens ?? agent.progress?.tokenCount;
|
||||
const toolUseCount = agent.result?.totalToolUseCount ?? agent.progress?.toolUseCount;
|
||||
const t6 = agent.selectedAgent?.agentType ?? "agent";
|
||||
const t7 = agent.description || "Async agent";
|
||||
let t8;
|
||||
if ($[11] !== t6 || $[12] !== t7) {
|
||||
t8 = <Text>{t6} ›{" "}{t7}</Text>;
|
||||
$[11] = t6;
|
||||
$[12] = t7;
|
||||
$[13] = t8;
|
||||
} else {
|
||||
t8 = $[13];
|
||||
}
|
||||
const title = t8;
|
||||
let t9;
|
||||
if ($[14] !== agent.status) {
|
||||
t9 = agent.status !== "running" && <Text color={getTaskStatusColor(agent.status)}>{getTaskStatusIcon(agent.status)}{" "}{agent.status === "completed" ? "Completed" : agent.status === "failed" ? "Failed" : "Stopped"}{" \xB7 "}</Text>;
|
||||
$[14] = agent.status;
|
||||
$[15] = t9;
|
||||
} else {
|
||||
t9 = $[15];
|
||||
}
|
||||
let t10;
|
||||
if ($[16] !== tokenCount) {
|
||||
t10 = tokenCount !== undefined && tokenCount > 0 && <> · {formatNumber(tokenCount)} tokens</>;
|
||||
$[16] = tokenCount;
|
||||
$[17] = t10;
|
||||
} else {
|
||||
t10 = $[17];
|
||||
}
|
||||
let t11;
|
||||
if ($[18] !== toolUseCount) {
|
||||
t11 = toolUseCount !== undefined && toolUseCount > 0 && <>{" "}· {toolUseCount} {toolUseCount === 1 ? "tool" : "tools"}</>;
|
||||
$[18] = toolUseCount;
|
||||
$[19] = t11;
|
||||
} else {
|
||||
t11 = $[19];
|
||||
}
|
||||
let t12;
|
||||
if ($[20] !== elapsedTime || $[21] !== t10 || $[22] !== t11) {
|
||||
t12 = <Text dimColor={true}>{elapsedTime}{t10}{t11}</Text>;
|
||||
$[20] = elapsedTime;
|
||||
$[21] = t10;
|
||||
$[22] = t11;
|
||||
$[23] = t12;
|
||||
} else {
|
||||
t12 = $[23];
|
||||
}
|
||||
let t13;
|
||||
if ($[24] !== t12 || $[25] !== t9) {
|
||||
t13 = <Text>{t9}{t12}</Text>;
|
||||
$[24] = t12;
|
||||
$[25] = t9;
|
||||
$[26] = t13;
|
||||
} else {
|
||||
t13 = $[26];
|
||||
}
|
||||
const subtitle = t13;
|
||||
let t14;
|
||||
if ($[27] !== agent.status || $[28] !== onBack || $[29] !== onKillAgent) {
|
||||
t14 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{agent.status === "running" && onKillAgent && <KeyboardShortcutHint shortcut="x" action="stop" />}</Byline>;
|
||||
$[27] = agent.status;
|
||||
$[28] = onBack;
|
||||
$[29] = onKillAgent;
|
||||
$[30] = t14;
|
||||
} else {
|
||||
t14 = $[30];
|
||||
}
|
||||
let t15;
|
||||
if ($[31] !== agent.progress || $[32] !== agent.status || $[33] !== theme) {
|
||||
t15 = agent.status === "running" && agent.progress?.recentActivities && agent.progress.recentActivities.length > 0 && <Box flexDirection="column"><Text bold={true} dimColor={true}>Progress</Text>{agent.progress.recentActivities.map((activity, i) => <Text key={i} dimColor={i < agent.progress.recentActivities.length - 1} wrap="truncate-end">{i === agent.progress.recentActivities.length - 1 ? "\u203A " : " "}{renderToolActivity(activity, tools, theme)}</Text>)}</Box>;
|
||||
$[31] = agent.progress;
|
||||
$[32] = agent.status;
|
||||
$[33] = theme;
|
||||
$[34] = t15;
|
||||
} else {
|
||||
t15 = $[34];
|
||||
}
|
||||
let t16;
|
||||
if ($[35] !== displayPrompt || $[36] !== planContent) {
|
||||
t16 = planContent ? <Box marginTop={1}><UserPlanMessage addMargin={false} planContent={planContent} /></Box> : <Box flexDirection="column" marginTop={1}><Text bold={true} dimColor={true}>Prompt</Text><Text wrap="wrap">{displayPrompt}</Text></Box>;
|
||||
$[35] = displayPrompt;
|
||||
$[36] = planContent;
|
||||
$[37] = t16;
|
||||
} else {
|
||||
t16 = $[37];
|
||||
}
|
||||
let t17;
|
||||
if ($[38] !== agent.error || $[39] !== agent.status) {
|
||||
t17 = agent.status === "failed" && agent.error && <Box flexDirection="column" marginTop={1}><Text bold={true} color="error">Error</Text><Text color="error" wrap="wrap">{agent.error}</Text></Box>;
|
||||
$[38] = agent.error;
|
||||
$[39] = agent.status;
|
||||
$[40] = t17;
|
||||
} else {
|
||||
t17 = $[40];
|
||||
}
|
||||
let t18;
|
||||
if ($[41] !== t15 || $[42] !== t16 || $[43] !== t17) {
|
||||
t18 = <Box flexDirection="column">{t15}{t16}{t17}</Box>;
|
||||
$[41] = t15;
|
||||
$[42] = t16;
|
||||
$[43] = t17;
|
||||
$[44] = t18;
|
||||
} else {
|
||||
t18 = $[44];
|
||||
}
|
||||
let t19;
|
||||
if ($[45] !== onDone || $[46] !== subtitle || $[47] !== t14 || $[48] !== t18 || $[49] !== title) {
|
||||
t19 = <Dialog title={title} subtitle={subtitle} onCancel={onDone} color="background" inputGuide={t14}>{t18}</Dialog>;
|
||||
$[45] = onDone;
|
||||
$[46] = subtitle;
|
||||
$[47] = t14;
|
||||
$[48] = t18;
|
||||
$[49] = title;
|
||||
$[50] = t19;
|
||||
} else {
|
||||
t19 = $[50];
|
||||
}
|
||||
let t20;
|
||||
if ($[51] !== handleKeyDown || $[52] !== t19) {
|
||||
t20 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t19}</Box>;
|
||||
$[51] = handleKeyDown;
|
||||
$[52] = t19;
|
||||
$[53] = t20;
|
||||
} else {
|
||||
t20 = $[53];
|
||||
}
|
||||
return t20;
|
||||
agent: DeepImmutable<LocalAgentTaskState>
|
||||
onDone: () => void
|
||||
onKillAgent?: () => void
|
||||
onBack?: () => void
|
||||
}
|
||||
|
||||
export function AsyncAgentDetailDialog({
|
||||
agent,
|
||||
onDone,
|
||||
onKillAgent,
|
||||
onBack,
|
||||
}: Props): React.ReactNode {
|
||||
const [theme] = useTheme()
|
||||
|
||||
// Get tools for rendering activity messages
|
||||
const tools = useMemo(() => getTools(getEmptyToolPermissionContext()), [])
|
||||
|
||||
const elapsedTime = useElapsedTime(
|
||||
agent.startTime,
|
||||
agent.status === 'running',
|
||||
1000,
|
||||
agent.totalPausedMs ?? 0,
|
||||
)
|
||||
|
||||
// Restore confirm:yes (Enter/y) dismissal — Dialog handles confirm:no (Esc)
|
||||
// internally but does NOT auto-wire confirm:yes.
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:yes': onDone,
|
||||
},
|
||||
{ context: 'Confirmation' },
|
||||
)
|
||||
|
||||
// Component-specific shortcuts shown in UI hints (x=stop) and
|
||||
// navigation keys (space=dismiss, left=back). These are context-dependent
|
||||
// actions tied to agent state, not standard dialog keybindings.
|
||||
// Note: Dialog component already handles ESC via confirm:no keybinding;
|
||||
// confirm:yes (Enter/y) is handled by useKeybindings above.
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onDone()
|
||||
} else if (e.key === 'left' && onBack) {
|
||||
e.preventDefault()
|
||||
onBack()
|
||||
} else if (e.key === 'x' && agent.status === 'running' && onKillAgent) {
|
||||
e.preventDefault()
|
||||
onKillAgent()
|
||||
}
|
||||
}
|
||||
|
||||
// Extract plan from prompt - if present, we show the plan instead of the prompt
|
||||
const planContent = extractTag(agent.prompt, 'plan')
|
||||
|
||||
const displayPrompt =
|
||||
agent.prompt.length > 300
|
||||
? agent.prompt.substring(0, 297) + '…'
|
||||
: agent.prompt
|
||||
|
||||
// Get tokens and tool uses (from result if completed, otherwise from progress)
|
||||
const tokenCount = agent.result?.totalTokens ?? agent.progress?.tokenCount
|
||||
const toolUseCount =
|
||||
agent.result?.totalToolUseCount ?? agent.progress?.toolUseCount
|
||||
|
||||
const title = (
|
||||
<Text>
|
||||
{agent.selectedAgent?.agentType ?? 'agent'} ›{' '}
|
||||
{agent.description || 'Async agent'}
|
||||
</Text>
|
||||
)
|
||||
|
||||
// Build subtitle with status and stats
|
||||
const subtitle = (
|
||||
<Text>
|
||||
{agent.status !== 'running' && (
|
||||
<Text color={getTaskStatusColor(agent.status)}>
|
||||
{getTaskStatusIcon(agent.status)}{' '}
|
||||
{agent.status === 'completed'
|
||||
? 'Completed'
|
||||
: agent.status === 'failed'
|
||||
? 'Failed'
|
||||
: 'Stopped'}
|
||||
{' · '}
|
||||
</Text>
|
||||
)}
|
||||
<Text dimColor>
|
||||
{elapsedTime}
|
||||
{tokenCount !== undefined && tokenCount > 0 && (
|
||||
<> · {formatNumber(tokenCount)} tokens</>
|
||||
)}
|
||||
{toolUseCount !== undefined && toolUseCount > 0 && (
|
||||
<>
|
||||
{' '}
|
||||
· {toolUseCount} {toolUseCount === 1 ? 'tool' : 'tools'}
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</Text>
|
||||
)
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Dialog
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
onCancel={onDone}
|
||||
color="background"
|
||||
inputGuide={exitState =>
|
||||
exitState.pending ? (
|
||||
<Text>Press {exitState.keyName} again to exit</Text>
|
||||
) : (
|
||||
<Byline>
|
||||
{onBack && <KeyboardShortcutHint shortcut="←" action="go back" />}
|
||||
<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />
|
||||
{agent.status === 'running' && onKillAgent && (
|
||||
<KeyboardShortcutHint shortcut="x" action="stop" />
|
||||
)}
|
||||
</Byline>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
{/* Recent activities for running agents */}
|
||||
{agent.status === 'running' &&
|
||||
agent.progress?.recentActivities &&
|
||||
agent.progress.recentActivities.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
<Text bold dimColor>
|
||||
Progress
|
||||
</Text>
|
||||
{agent.progress.recentActivities.map((activity, i) => (
|
||||
<Text
|
||||
key={i}
|
||||
dimColor={i < agent.progress!.recentActivities!.length - 1}
|
||||
wrap="truncate-end"
|
||||
>
|
||||
{i === agent.progress!.recentActivities!.length - 1
|
||||
? '› '
|
||||
: ' '}
|
||||
{renderToolActivity(activity, tools, theme)}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Plan section (if present) - shown instead of prompt */}
|
||||
{planContent ? (
|
||||
<Box marginTop={1}>
|
||||
<UserPlanMessage addMargin={false} planContent={planContent} />
|
||||
</Box>
|
||||
) : (
|
||||
/* Prompt section - only shown when no plan */
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text bold dimColor>
|
||||
Prompt
|
||||
</Text>
|
||||
<Text wrap="wrap">{displayPrompt}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Error details if failed */}
|
||||
{agent.status === 'failed' && agent.error && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text bold color="error">
|
||||
Error
|
||||
</Text>
|
||||
<Text color="error" wrap="wrap">
|
||||
{agent.error}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Dialog>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,344 +1,146 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Text } from 'src/ink.js';
|
||||
import type { BackgroundTaskState } from 'src/tasks/types.js';
|
||||
import type { DeepImmutable } from 'src/types/utils.js';
|
||||
import { truncate } from 'src/utils/format.js';
|
||||
import { toInkColor } from 'src/utils/ink.js';
|
||||
import { plural } from 'src/utils/stringUtils.js';
|
||||
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js';
|
||||
import { RemoteSessionProgress } from './RemoteSessionProgress.js';
|
||||
import { ShellProgress, TaskStatusText } from './ShellProgress.js';
|
||||
import { describeTeammateActivity } from './taskStatusUtils.js';
|
||||
import * as React from 'react'
|
||||
import { Text } from 'src/ink.js'
|
||||
import type { BackgroundTaskState } from 'src/tasks/types.js'
|
||||
import type { DeepImmutable } from 'src/types/utils.js'
|
||||
import { truncate } from 'src/utils/format.js'
|
||||
import { toInkColor } from 'src/utils/ink.js'
|
||||
import { plural } from 'src/utils/stringUtils.js'
|
||||
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'
|
||||
import { RemoteSessionProgress } from './RemoteSessionProgress.js'
|
||||
import { ShellProgress, TaskStatusText } from './ShellProgress.js'
|
||||
import { describeTeammateActivity } from './taskStatusUtils.js'
|
||||
|
||||
type Props = {
|
||||
task: DeepImmutable<BackgroundTaskState>;
|
||||
maxActivityWidth?: number;
|
||||
};
|
||||
export function BackgroundTask(t0) {
|
||||
const $ = _c(92);
|
||||
const {
|
||||
task,
|
||||
maxActivityWidth
|
||||
} = t0;
|
||||
const activityLimit = maxActivityWidth ?? 40;
|
||||
task: DeepImmutable<BackgroundTaskState>
|
||||
maxActivityWidth?: number
|
||||
}
|
||||
|
||||
export function BackgroundTask({
|
||||
task,
|
||||
maxActivityWidth,
|
||||
}: Props): React.ReactNode {
|
||||
const activityLimit = maxActivityWidth ?? 40
|
||||
switch (task.type) {
|
||||
case "local_bash":
|
||||
{
|
||||
const t1 = task.kind === "monitor" ? task.description : task.command;
|
||||
let t2;
|
||||
if ($[0] !== activityLimit || $[1] !== t1) {
|
||||
t2 = truncate(t1, activityLimit, true);
|
||||
$[0] = activityLimit;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== task) {
|
||||
t3 = <ShellProgress shell={task} />;
|
||||
$[3] = task;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== t2 || $[6] !== t3) {
|
||||
t4 = <Text>{t2}{" "}{t3}</Text>;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
case "remote_agent":
|
||||
{
|
||||
if (task.isRemoteReview) {
|
||||
let t1;
|
||||
if ($[8] !== task) {
|
||||
t1 = <Text><RemoteSessionProgress session={task} /></Text>;
|
||||
$[8] = task;
|
||||
$[9] = t1;
|
||||
} else {
|
||||
t1 = $[9];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
const running = task.status === "running" || task.status === "pending";
|
||||
const t1 = running ? DIAMOND_OPEN : DIAMOND_FILLED;
|
||||
let t2;
|
||||
if ($[10] !== t1) {
|
||||
t2 = <Text dimColor={true}>{t1} </Text>;
|
||||
$[10] = t1;
|
||||
$[11] = t2;
|
||||
} else {
|
||||
t2 = $[11];
|
||||
}
|
||||
let t3;
|
||||
if ($[12] !== activityLimit || $[13] !== task.title) {
|
||||
t3 = truncate(task.title, activityLimit, true);
|
||||
$[12] = activityLimit;
|
||||
$[13] = task.title;
|
||||
$[14] = t3;
|
||||
} else {
|
||||
t3 = $[14];
|
||||
}
|
||||
let t4;
|
||||
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Text dimColor={true}> · </Text>;
|
||||
$[15] = t4;
|
||||
} else {
|
||||
t4 = $[15];
|
||||
}
|
||||
let t5;
|
||||
if ($[16] !== task) {
|
||||
t5 = <RemoteSessionProgress session={task} />;
|
||||
$[16] = task;
|
||||
$[17] = t5;
|
||||
} else {
|
||||
t5 = $[17];
|
||||
}
|
||||
let t6;
|
||||
if ($[18] !== t2 || $[19] !== t3 || $[20] !== t5) {
|
||||
t6 = <Text>{t2}{t3}{t4}{t5}</Text>;
|
||||
$[18] = t2;
|
||||
$[19] = t3;
|
||||
$[20] = t5;
|
||||
$[21] = t6;
|
||||
} else {
|
||||
t6 = $[21];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
case "local_agent":
|
||||
{
|
||||
let t1;
|
||||
if ($[22] !== activityLimit || $[23] !== task.description) {
|
||||
t1 = truncate(task.description, activityLimit, true);
|
||||
$[22] = activityLimit;
|
||||
$[23] = task.description;
|
||||
$[24] = t1;
|
||||
} else {
|
||||
t1 = $[24];
|
||||
}
|
||||
const t2 = task.status === "completed" ? "done" : undefined;
|
||||
const t3 = task.status === "completed" && !task.notified ? ", unread" : undefined;
|
||||
let t4;
|
||||
if ($[25] !== t2 || $[26] !== t3 || $[27] !== task.status) {
|
||||
t4 = <TaskStatusText status={task.status} label={t2} suffix={t3} />;
|
||||
$[25] = t2;
|
||||
$[26] = t3;
|
||||
$[27] = task.status;
|
||||
$[28] = t4;
|
||||
} else {
|
||||
t4 = $[28];
|
||||
}
|
||||
let t5;
|
||||
if ($[29] !== t1 || $[30] !== t4) {
|
||||
t5 = <Text>{t1}{" "}{t4}</Text>;
|
||||
$[29] = t1;
|
||||
$[30] = t4;
|
||||
$[31] = t5;
|
||||
} else {
|
||||
t5 = $[31];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
case "in_process_teammate":
|
||||
{
|
||||
let T0;
|
||||
let T1;
|
||||
let t1;
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[32] !== activityLimit || $[33] !== task) {
|
||||
const activity = describeTeammateActivity(task);
|
||||
T1 = Text;
|
||||
let t5;
|
||||
if ($[40] !== task.identity.color) {
|
||||
t5 = toInkColor(task.identity.color);
|
||||
$[40] = task.identity.color;
|
||||
$[41] = t5;
|
||||
} else {
|
||||
t5 = $[41];
|
||||
}
|
||||
if ($[42] !== t5 || $[43] !== task.identity.agentName) {
|
||||
t4 = <Text color={t5}>@{task.identity.agentName}</Text>;
|
||||
$[42] = t5;
|
||||
$[43] = task.identity.agentName;
|
||||
$[44] = t4;
|
||||
} else {
|
||||
t4 = $[44];
|
||||
}
|
||||
T0 = Text;
|
||||
t1 = true;
|
||||
t2 = ": ";
|
||||
t3 = truncate(activity, activityLimit, true);
|
||||
$[32] = activityLimit;
|
||||
$[33] = task;
|
||||
$[34] = T0;
|
||||
$[35] = T1;
|
||||
$[36] = t1;
|
||||
$[37] = t2;
|
||||
$[38] = t3;
|
||||
$[39] = t4;
|
||||
} else {
|
||||
T0 = $[34];
|
||||
T1 = $[35];
|
||||
t1 = $[36];
|
||||
t2 = $[37];
|
||||
t3 = $[38];
|
||||
t4 = $[39];
|
||||
}
|
||||
let t5;
|
||||
if ($[45] !== T0 || $[46] !== t1 || $[47] !== t2 || $[48] !== t3) {
|
||||
t5 = <T0 dimColor={t1}>{t2}{t3}</T0>;
|
||||
$[45] = T0;
|
||||
$[46] = t1;
|
||||
$[47] = t2;
|
||||
$[48] = t3;
|
||||
$[49] = t5;
|
||||
} else {
|
||||
t5 = $[49];
|
||||
}
|
||||
let t6;
|
||||
if ($[50] !== T1 || $[51] !== t4 || $[52] !== t5) {
|
||||
t6 = <T1>{t4}{t5}</T1>;
|
||||
$[50] = T1;
|
||||
$[51] = t4;
|
||||
$[52] = t5;
|
||||
$[53] = t6;
|
||||
} else {
|
||||
t6 = $[53];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
case "local_workflow":
|
||||
{
|
||||
const t1 = task.workflowName ?? task.summary ?? task.description;
|
||||
let t2;
|
||||
if ($[54] !== activityLimit || $[55] !== t1) {
|
||||
t2 = truncate(t1, activityLimit, true);
|
||||
$[54] = activityLimit;
|
||||
$[55] = t1;
|
||||
$[56] = t2;
|
||||
} else {
|
||||
t2 = $[56];
|
||||
}
|
||||
let t3;
|
||||
if ($[57] !== task.agentCount || $[58] !== task.status) {
|
||||
t3 = task.status === "running" ? `${task.agentCount} ${plural(task.agentCount, "agent")}` : task.status === "completed" ? "done" : undefined;
|
||||
$[57] = task.agentCount;
|
||||
$[58] = task.status;
|
||||
$[59] = t3;
|
||||
} else {
|
||||
t3 = $[59];
|
||||
}
|
||||
const t4 = task.status === "completed" && !task.notified ? ", unread" : undefined;
|
||||
let t5;
|
||||
if ($[60] !== t3 || $[61] !== t4 || $[62] !== task.status) {
|
||||
t5 = <TaskStatusText status={task.status} label={t3} suffix={t4} />;
|
||||
$[60] = t3;
|
||||
$[61] = t4;
|
||||
$[62] = task.status;
|
||||
$[63] = t5;
|
||||
} else {
|
||||
t5 = $[63];
|
||||
}
|
||||
let t6;
|
||||
if ($[64] !== t2 || $[65] !== t5) {
|
||||
t6 = <Text>{t2}{" "}{t5}</Text>;
|
||||
$[64] = t2;
|
||||
$[65] = t5;
|
||||
$[66] = t6;
|
||||
} else {
|
||||
t6 = $[66];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
case "monitor_mcp":
|
||||
{
|
||||
let t1;
|
||||
if ($[67] !== activityLimit || $[68] !== task.description) {
|
||||
t1 = truncate(task.description, activityLimit, true);
|
||||
$[67] = activityLimit;
|
||||
$[68] = task.description;
|
||||
$[69] = t1;
|
||||
} else {
|
||||
t1 = $[69];
|
||||
}
|
||||
const t2 = task.status === "completed" ? "done" : undefined;
|
||||
const t3 = task.status === "completed" && !task.notified ? ", unread" : undefined;
|
||||
let t4;
|
||||
if ($[70] !== t2 || $[71] !== t3 || $[72] !== task.status) {
|
||||
t4 = <TaskStatusText status={task.status} label={t2} suffix={t3} />;
|
||||
$[70] = t2;
|
||||
$[71] = t3;
|
||||
$[72] = task.status;
|
||||
$[73] = t4;
|
||||
} else {
|
||||
t4 = $[73];
|
||||
}
|
||||
let t5;
|
||||
if ($[74] !== t1 || $[75] !== t4) {
|
||||
t5 = <Text>{t1}{" "}{t4}</Text>;
|
||||
$[74] = t1;
|
||||
$[75] = t4;
|
||||
$[76] = t5;
|
||||
} else {
|
||||
t5 = $[76];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
case "dream":
|
||||
{
|
||||
const n = task.filesTouched.length;
|
||||
let t1;
|
||||
if ($[77] !== n || $[78] !== task.phase || $[79] !== task.sessionsReviewing) {
|
||||
t1 = task.phase === "updating" && n > 0 ? `${n} ${plural(n, "file")}` : `${task.sessionsReviewing} ${plural(task.sessionsReviewing, "session")}`;
|
||||
$[77] = n;
|
||||
$[78] = task.phase;
|
||||
$[79] = task.sessionsReviewing;
|
||||
$[80] = t1;
|
||||
} else {
|
||||
t1 = $[80];
|
||||
}
|
||||
const detail = t1;
|
||||
let t2;
|
||||
if ($[81] !== detail || $[82] !== task.phase) {
|
||||
t2 = <Text dimColor={true}>· {task.phase} · {detail}</Text>;
|
||||
$[81] = detail;
|
||||
$[82] = task.phase;
|
||||
$[83] = t2;
|
||||
} else {
|
||||
t2 = $[83];
|
||||
}
|
||||
const t3 = task.status === "completed" ? "done" : undefined;
|
||||
const t4 = task.status === "completed" && !task.notified ? ", unread" : undefined;
|
||||
let t5;
|
||||
if ($[84] !== t3 || $[85] !== t4 || $[86] !== task.status) {
|
||||
t5 = <TaskStatusText status={task.status} label={t3} suffix={t4} />;
|
||||
$[84] = t3;
|
||||
$[85] = t4;
|
||||
$[86] = task.status;
|
||||
$[87] = t5;
|
||||
} else {
|
||||
t5 = $[87];
|
||||
}
|
||||
let t6;
|
||||
if ($[88] !== t2 || $[89] !== t5 || $[90] !== task.description) {
|
||||
t6 = <Text>{task.description}{" "}{t2}{" "}{t5}</Text>;
|
||||
$[88] = t2;
|
||||
$[89] = t5;
|
||||
$[90] = task.description;
|
||||
$[91] = t6;
|
||||
} else {
|
||||
t6 = $[91];
|
||||
}
|
||||
return t6;
|
||||
case 'local_bash':
|
||||
return (
|
||||
<Text>
|
||||
{truncate(
|
||||
task.kind === 'monitor' ? task.description : task.command,
|
||||
activityLimit,
|
||||
true,
|
||||
)}{' '}
|
||||
<ShellProgress shell={task} />
|
||||
</Text>
|
||||
)
|
||||
case 'remote_agent': {
|
||||
// Lite-review renders its own rainbow line (title + live counts),
|
||||
// so we don't prefix the title — the rainbow already includes it.
|
||||
if (task.isRemoteReview) {
|
||||
return (
|
||||
<Text>
|
||||
<RemoteSessionProgress session={task} />
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
const running = task.status === 'running' || task.status === 'pending'
|
||||
return (
|
||||
<Text>
|
||||
<Text dimColor>{running ? DIAMOND_OPEN : DIAMOND_FILLED} </Text>
|
||||
{truncate(task.title, activityLimit, true)}
|
||||
<Text dimColor> · </Text>
|
||||
<RemoteSessionProgress session={task} />
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case 'local_agent':
|
||||
return (
|
||||
<Text>
|
||||
{truncate(task.description, activityLimit, true)}{' '}
|
||||
<TaskStatusText
|
||||
status={task.status}
|
||||
label={task.status === 'completed' ? 'done' : undefined}
|
||||
suffix={
|
||||
task.status === 'completed' && !task.notified
|
||||
? ', unread'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
)
|
||||
case 'in_process_teammate': {
|
||||
const activity = describeTeammateActivity(task)
|
||||
return (
|
||||
<Text>
|
||||
<Text color={toInkColor(task.identity.color)}>
|
||||
@{task.identity.agentName}
|
||||
</Text>
|
||||
<Text dimColor>: {truncate(activity, activityLimit, true)}</Text>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
case 'local_workflow':
|
||||
return (
|
||||
<Text>
|
||||
{truncate(
|
||||
task.workflowName ?? task.summary ?? task.description,
|
||||
activityLimit,
|
||||
true,
|
||||
)}{' '}
|
||||
<TaskStatusText
|
||||
status={task.status}
|
||||
label={
|
||||
task.status === 'running'
|
||||
? `${task.agentCount} ${plural(task.agentCount, 'agent')}`
|
||||
: task.status === 'completed'
|
||||
? 'done'
|
||||
: undefined
|
||||
}
|
||||
suffix={
|
||||
task.status === 'completed' && !task.notified
|
||||
? ', unread'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
)
|
||||
case 'monitor_mcp':
|
||||
return (
|
||||
<Text>
|
||||
{truncate(task.description, activityLimit, true)}{' '}
|
||||
<TaskStatusText
|
||||
status={task.status}
|
||||
label={task.status === 'completed' ? 'done' : undefined}
|
||||
suffix={
|
||||
task.status === 'completed' && !task.notified
|
||||
? ', unread'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
)
|
||||
case 'dream': {
|
||||
const n = task.filesTouched.length
|
||||
const detail =
|
||||
task.phase === 'updating' && n > 0
|
||||
? `${n} ${plural(n, 'file')}`
|
||||
: `${task.sessionsReviewing} ${plural(task.sessionsReviewing, 'session')}`
|
||||
return (
|
||||
<Text>
|
||||
{task.description}{' '}
|
||||
<Text dimColor>
|
||||
· {task.phase} · {detail}
|
||||
</Text>{' '}
|
||||
<TaskStatusText
|
||||
status={task.status}
|
||||
label={task.status === 'completed' ? 'done' : undefined}
|
||||
suffix={
|
||||
task.status === 'completed' && !task.notified
|
||||
? ', unread'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,428 +1,310 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTerminalSize } from 'src/hooks/useTerminalSize.js';
|
||||
import { stringWidth } from 'src/ink/stringWidth.js';
|
||||
import { useAppState, useSetAppState } from 'src/state/AppState.js';
|
||||
import { enterTeammateView, exitTeammateView } from 'src/state/teammateViewHelpers.js';
|
||||
import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js';
|
||||
import { getPillLabel, pillNeedsCta } from 'src/tasks/pillLabel.js';
|
||||
import { type BackgroundTaskState, isBackgroundTask, type TaskState } from 'src/tasks/types.js';
|
||||
import { calculateHorizontalScrollWindow } from 'src/utils/horizontalScroll.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from '../../tools/AgentTool/agentColorManager.js';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
||||
import { shouldHideTasksFooter } from './taskStatusUtils.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTerminalSize } from 'src/hooks/useTerminalSize.js'
|
||||
import { stringWidth } from 'src/ink/stringWidth.js'
|
||||
import { useAppState, useSetAppState } from 'src/state/AppState.js'
|
||||
import {
|
||||
enterTeammateView,
|
||||
exitTeammateView,
|
||||
} from 'src/state/teammateViewHelpers.js'
|
||||
import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
|
||||
import { getPillLabel, pillNeedsCta } from 'src/tasks/pillLabel.js'
|
||||
import {
|
||||
type BackgroundTaskState,
|
||||
isBackgroundTask,
|
||||
type TaskState,
|
||||
} from 'src/tasks/types.js'
|
||||
import { calculateHorizontalScrollWindow } from 'src/utils/horizontalScroll.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import {
|
||||
AGENT_COLOR_TO_THEME_COLOR,
|
||||
AGENT_COLORS,
|
||||
type AgentColorName,
|
||||
} from '../../tools/AgentTool/agentColorManager.js'
|
||||
import type { Theme } from '../../utils/theme.js'
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||
import { shouldHideTasksFooter } from './taskStatusUtils.js'
|
||||
|
||||
type Props = {
|
||||
tasksSelected: boolean;
|
||||
isViewingTeammate?: boolean;
|
||||
teammateFooterIndex?: number;
|
||||
isLeaderIdle?: boolean;
|
||||
onOpenDialog?: (taskId?: string) => void;
|
||||
};
|
||||
export function BackgroundTaskStatus(t0) {
|
||||
const $ = _c(48);
|
||||
const {
|
||||
tasksSelected,
|
||||
isViewingTeammate,
|
||||
teammateFooterIndex: t1,
|
||||
isLeaderIdle: t2,
|
||||
onOpenDialog
|
||||
} = t0;
|
||||
const teammateFooterIndex = t1 === undefined ? 0 : t1;
|
||||
const isLeaderIdle = t2 === undefined ? false : t2;
|
||||
const setAppState = useSetAppState();
|
||||
const {
|
||||
columns
|
||||
} = useTerminalSize();
|
||||
const tasks = useAppState(_temp);
|
||||
const viewingAgentTaskId = useAppState(_temp2);
|
||||
let t3;
|
||||
if ($[0] !== tasks) {
|
||||
t3 = (Object.values(tasks ?? {}) as TaskState[]).filter(_temp3);
|
||||
$[0] = tasks;
|
||||
$[1] = t3;
|
||||
} else {
|
||||
t3 = $[1];
|
||||
}
|
||||
const runningTasks = t3;
|
||||
const expandedView = useAppState(_temp4);
|
||||
const showSpinnerTree = expandedView === "teammates";
|
||||
const allTeammates = !showSpinnerTree && runningTasks.length > 0 && runningTasks.every(_temp5);
|
||||
let t4;
|
||||
if ($[2] !== runningTasks) {
|
||||
t4 = runningTasks.filter(_temp6).sort(_temp7);
|
||||
$[2] = runningTasks;
|
||||
$[3] = t4;
|
||||
} else {
|
||||
t4 = $[3];
|
||||
}
|
||||
const teammateEntries = t4;
|
||||
let t5;
|
||||
if ($[4] !== isLeaderIdle) {
|
||||
t5 = {
|
||||
name: "main",
|
||||
tasksSelected: boolean
|
||||
isViewingTeammate?: boolean
|
||||
teammateFooterIndex?: number
|
||||
isLeaderIdle?: boolean
|
||||
onOpenDialog?: (taskId?: string) => void
|
||||
}
|
||||
|
||||
export function BackgroundTaskStatus({
|
||||
tasksSelected,
|
||||
isViewingTeammate,
|
||||
teammateFooterIndex = 0,
|
||||
isLeaderIdle = false,
|
||||
onOpenDialog,
|
||||
}: Props): React.ReactNode {
|
||||
const setAppState = useSetAppState()
|
||||
const { columns } = useTerminalSize()
|
||||
const tasks = useAppState(s => s.tasks)
|
||||
const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId)
|
||||
|
||||
const runningTasks = useMemo(
|
||||
() =>
|
||||
(Object.values(tasks ?? {}) as TaskState[]).filter(
|
||||
t =>
|
||||
isBackgroundTask(t) &&
|
||||
!(process.env.USER_TYPE === 'ant' && isPanelAgentTask(t)),
|
||||
),
|
||||
[tasks],
|
||||
)
|
||||
|
||||
// Check if all tasks are in-process teammates (team mode)
|
||||
// In spinner-tree mode, don't show teammate pills (teammates appear in the spinner tree)
|
||||
const expandedView = useAppState(s => s.expandedView)
|
||||
const showSpinnerTree = expandedView === 'teammates'
|
||||
const allTeammates =
|
||||
!showSpinnerTree &&
|
||||
runningTasks.length > 0 &&
|
||||
runningTasks.every(t => t.type === 'in_process_teammate')
|
||||
|
||||
// Memoize teammate-related computations at the top level (rules of hooks)
|
||||
const teammateEntries = useMemo(
|
||||
() =>
|
||||
runningTasks
|
||||
.filter(
|
||||
(t): t is BackgroundTaskState & { type: 'in_process_teammate' } =>
|
||||
t.type === 'in_process_teammate',
|
||||
)
|
||||
.sort((a, b) =>
|
||||
a.identity.agentName.localeCompare(b.identity.agentName),
|
||||
),
|
||||
[runningTasks],
|
||||
)
|
||||
|
||||
// Build array of all pills with their activity state
|
||||
// Each pill is "@{name}" and separator is " " (1 char)
|
||||
// Sort idle agents to the end, but only when not in selection mode
|
||||
// to avoid reordering while user is arrowing through the list
|
||||
// "main" always stays first regardless of idle state
|
||||
const allPills = useMemo(() => {
|
||||
const mainPill = {
|
||||
name: 'main',
|
||||
color: undefined as keyof Theme | undefined,
|
||||
isIdle: isLeaderIdle,
|
||||
taskId: undefined as string | undefined
|
||||
};
|
||||
$[4] = isLeaderIdle;
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
}
|
||||
const mainPill = t5;
|
||||
let t6;
|
||||
if ($[6] !== mainPill || $[7] !== tasksSelected || $[8] !== teammateEntries) {
|
||||
const teammatePills = teammateEntries.map(_temp8);
|
||||
taskId: undefined as string | undefined,
|
||||
}
|
||||
|
||||
const teammatePills = teammateEntries.map(t => ({
|
||||
name: t.identity.agentName,
|
||||
color: getAgentThemeColor(t.identity.color),
|
||||
isIdle: t.isIdle,
|
||||
taskId: t.id,
|
||||
}))
|
||||
|
||||
// Only sort teammates when not selecting to avoid reordering during navigation
|
||||
if (!tasksSelected) {
|
||||
teammatePills.sort(_temp9);
|
||||
teammatePills.sort((a, b) => {
|
||||
// Active agents first, idle agents last
|
||||
if (a.isIdle !== b.isIdle) return a.isIdle ? 1 : -1
|
||||
return 0 // Keep original order within each group
|
||||
})
|
||||
}
|
||||
const pills = [mainPill, ...teammatePills];
|
||||
t6 = pills.map(_temp0);
|
||||
$[6] = mainPill;
|
||||
$[7] = tasksSelected;
|
||||
$[8] = teammateEntries;
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
const allPills = t6;
|
||||
let t7;
|
||||
if ($[10] !== allPills) {
|
||||
t7 = allPills.map(_temp1);
|
||||
$[10] = allPills;
|
||||
$[11] = t7;
|
||||
} else {
|
||||
t7 = $[11];
|
||||
}
|
||||
const pillWidths = t7;
|
||||
if (allTeammates || !showSpinnerTree && isViewingTeammate) {
|
||||
const selectedIdx = tasksSelected ? teammateFooterIndex : -1;
|
||||
let t8;
|
||||
if ($[12] !== teammateEntries || $[13] !== viewingAgentTaskId) {
|
||||
t8 = viewingAgentTaskId ? teammateEntries.findIndex(t_3 => t_3.id === viewingAgentTaskId) + 1 : 0;
|
||||
$[12] = teammateEntries;
|
||||
$[13] = viewingAgentTaskId;
|
||||
$[14] = t8;
|
||||
} else {
|
||||
t8 = $[14];
|
||||
}
|
||||
const viewedIdx = t8;
|
||||
const availableWidth = Math.max(20, columns - 20 - 4);
|
||||
const t9 = selectedIdx >= 0 ? selectedIdx : 0;
|
||||
let t10;
|
||||
if ($[15] !== availableWidth || $[16] !== pillWidths || $[17] !== t9) {
|
||||
t10 = calculateHorizontalScrollWindow(pillWidths, availableWidth, 2, t9);
|
||||
$[15] = availableWidth;
|
||||
$[16] = pillWidths;
|
||||
$[17] = t9;
|
||||
$[18] = t10;
|
||||
} else {
|
||||
t10 = $[18];
|
||||
}
|
||||
const {
|
||||
startIndex,
|
||||
endIndex,
|
||||
showLeftArrow,
|
||||
showRightArrow
|
||||
} = t10;
|
||||
let t11;
|
||||
if ($[19] !== allPills || $[20] !== endIndex || $[21] !== startIndex) {
|
||||
t11 = allPills.slice(startIndex, endIndex);
|
||||
$[19] = allPills;
|
||||
$[20] = endIndex;
|
||||
$[21] = startIndex;
|
||||
$[22] = t11;
|
||||
} else {
|
||||
t11 = $[22];
|
||||
}
|
||||
const visiblePills = t11;
|
||||
let t12;
|
||||
if ($[23] !== showLeftArrow) {
|
||||
t12 = showLeftArrow && <Text dimColor={true}>{figures.arrowLeft} </Text>;
|
||||
$[23] = showLeftArrow;
|
||||
$[24] = t12;
|
||||
} else {
|
||||
t12 = $[24];
|
||||
}
|
||||
let t13;
|
||||
if ($[25] !== selectedIdx || $[26] !== setAppState || $[27] !== viewedIdx || $[28] !== visiblePills) {
|
||||
t13 = visiblePills.map((pill_1, i_1) => {
|
||||
const needsSeparator = i_1 > 0;
|
||||
return <React.Fragment key={pill_1.name}>{needsSeparator && <Text> </Text>}<AgentPill name={pill_1.name} color={pill_1.color} isSelected={selectedIdx === pill_1.idx} isViewed={viewedIdx === pill_1.idx} isIdle={pill_1.isIdle} onClick={() => pill_1.taskId ? enterTeammateView(pill_1.taskId, setAppState) : exitTeammateView(setAppState)} /></React.Fragment>;
|
||||
});
|
||||
$[25] = selectedIdx;
|
||||
$[26] = setAppState;
|
||||
$[27] = viewedIdx;
|
||||
$[28] = visiblePills;
|
||||
$[29] = t13;
|
||||
} else {
|
||||
t13 = $[29];
|
||||
}
|
||||
let t14;
|
||||
if ($[30] !== showRightArrow) {
|
||||
t14 = showRightArrow && <Text dimColor={true}> {figures.arrowRight}</Text>;
|
||||
$[30] = showRightArrow;
|
||||
$[31] = t14;
|
||||
} else {
|
||||
t14 = $[31];
|
||||
}
|
||||
let t15;
|
||||
if ($[32] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t15 = <Text dimColor={true}>{" \xB7 "}<KeyboardShortcutHint shortcut={"shift + \u2193"} action="expand" /></Text>;
|
||||
$[32] = t15;
|
||||
} else {
|
||||
t15 = $[32];
|
||||
}
|
||||
let t16;
|
||||
if ($[33] !== t12 || $[34] !== t13 || $[35] !== t14) {
|
||||
t16 = <>{t12}{t13}{t14}{t15}</>;
|
||||
$[33] = t12;
|
||||
$[34] = t13;
|
||||
$[35] = t14;
|
||||
$[36] = t16;
|
||||
} else {
|
||||
t16 = $[36];
|
||||
}
|
||||
return t16;
|
||||
|
||||
// main always first, then sorted teammates
|
||||
const pills = [mainPill, ...teammatePills]
|
||||
|
||||
// Add idx after sorting
|
||||
return pills.map((pill, i) => ({ ...pill, idx: i }))
|
||||
}, [teammateEntries, isLeaderIdle, tasksSelected])
|
||||
|
||||
// Calculate pill widths (including separator space, except first)
|
||||
const pillWidths = useMemo(
|
||||
() =>
|
||||
allPills.map((pill, i) => {
|
||||
const pillText = `@${pill.name}`
|
||||
// First pill has no leading space, others have 1 space separator
|
||||
return stringWidth(pillText) + (i > 0 ? 1 : 0)
|
||||
}),
|
||||
[allPills],
|
||||
)
|
||||
|
||||
if (allTeammates || (!showSpinnerTree && isViewingTeammate)) {
|
||||
const selectedIdx = tasksSelected ? teammateFooterIndex : -1
|
||||
// Which agent is currently foregrounded (bold)
|
||||
const viewedIdx = viewingAgentTaskId
|
||||
? teammateEntries.findIndex(t => t.id === viewingAgentTaskId) + 1
|
||||
: 0 // 0 = main/leader
|
||||
|
||||
// Calculate available width for pills
|
||||
// Reserve space for: arrows, hint, and minimal padding
|
||||
// Pills are rendered on their own line when in team mode
|
||||
const ARROW_WIDTH = 2 // arrow char + space
|
||||
const HINT_WIDTH = 20 // shift+↓ to expand
|
||||
const PADDING = 4 // minimal safety margin
|
||||
const availableWidth = Math.max(20, columns - HINT_WIDTH - PADDING)
|
||||
|
||||
// Calculate visible window of pills
|
||||
const { startIndex, endIndex, showLeftArrow, showRightArrow } =
|
||||
calculateHorizontalScrollWindow(
|
||||
pillWidths,
|
||||
availableWidth,
|
||||
ARROW_WIDTH,
|
||||
selectedIdx >= 0 ? selectedIdx : 0,
|
||||
)
|
||||
|
||||
const visiblePills = allPills.slice(startIndex, endIndex)
|
||||
|
||||
return (
|
||||
<>
|
||||
{showLeftArrow && <Text dimColor>{figures.arrowLeft} </Text>}
|
||||
{visiblePills.map((pill, i) => {
|
||||
// First visible pill has no leading separator
|
||||
// (left arrow already provides spacing if present)
|
||||
const needsSeparator = i > 0
|
||||
return (
|
||||
<React.Fragment key={pill.name}>
|
||||
{needsSeparator && <Text> </Text>}
|
||||
<AgentPill
|
||||
name={pill.name}
|
||||
color={pill.color}
|
||||
isSelected={selectedIdx === pill.idx}
|
||||
isViewed={viewedIdx === pill.idx}
|
||||
isIdle={pill.isIdle}
|
||||
onClick={() =>
|
||||
pill.taskId
|
||||
? enterTeammateView(pill.taskId, setAppState)
|
||||
: exitTeammateView(setAppState)
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
{showRightArrow && <Text dimColor> {figures.arrowRight}</Text>}
|
||||
<Text dimColor>
|
||||
{' · '}
|
||||
<KeyboardShortcutHint shortcut="shift + ↓" action="expand" />
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// In spinner-tree mode, don't show any footer status for teammates
|
||||
// (they appear in the spinner tree above)
|
||||
if (shouldHideTasksFooter(tasks ?? {}, showSpinnerTree)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
if (runningTasks.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t8;
|
||||
if ($[37] !== runningTasks) {
|
||||
t8 = getPillLabel(runningTasks);
|
||||
$[37] = runningTasks;
|
||||
$[38] = t8;
|
||||
} else {
|
||||
t8 = $[38];
|
||||
}
|
||||
let t9;
|
||||
if ($[39] !== onOpenDialog || $[40] !== t8 || $[41] !== tasksSelected) {
|
||||
t9 = <SummaryPill selected={tasksSelected} onClick={onOpenDialog}>{t8}</SummaryPill>;
|
||||
$[39] = onOpenDialog;
|
||||
$[40] = t8;
|
||||
$[41] = tasksSelected;
|
||||
$[42] = t9;
|
||||
} else {
|
||||
t9 = $[42];
|
||||
}
|
||||
let t10;
|
||||
if ($[43] !== runningTasks) {
|
||||
t10 = pillNeedsCta(runningTasks) && <Text dimColor={true}> · {figures.arrowDown} to view</Text>;
|
||||
$[43] = runningTasks;
|
||||
$[44] = t10;
|
||||
} else {
|
||||
t10 = $[44];
|
||||
}
|
||||
let t11;
|
||||
if ($[45] !== t10 || $[46] !== t9) {
|
||||
t11 = <>{t9}{t10}</>;
|
||||
$[45] = t10;
|
||||
$[46] = t9;
|
||||
$[47] = t11;
|
||||
} else {
|
||||
t11 = $[47];
|
||||
}
|
||||
return t11;
|
||||
}
|
||||
function _temp1(pill_0, i_0) {
|
||||
const pillText = `@${pill_0.name}`;
|
||||
return stringWidth(pillText) + (i_0 > 0 ? 1 : 0);
|
||||
}
|
||||
function _temp0(pill, i) {
|
||||
return {
|
||||
...pill,
|
||||
idx: i
|
||||
};
|
||||
}
|
||||
function _temp9(a_0, b_0) {
|
||||
if (a_0.isIdle !== b_0.isIdle) {
|
||||
return a_0.isIdle ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function _temp8(t_2) {
|
||||
return {
|
||||
name: t_2.identity.agentName,
|
||||
color: getAgentThemeColor(t_2.identity.color),
|
||||
isIdle: t_2.isIdle,
|
||||
taskId: t_2.id
|
||||
};
|
||||
}
|
||||
function _temp7(a, b) {
|
||||
return a.identity.agentName.localeCompare(b.identity.agentName);
|
||||
}
|
||||
function _temp6(t_1) {
|
||||
return t_1.type === "in_process_teammate";
|
||||
}
|
||||
function _temp5(t_0) {
|
||||
return t_0.type === "in_process_teammate";
|
||||
}
|
||||
function _temp4(s_1) {
|
||||
return s_1.expandedView;
|
||||
}
|
||||
function _temp3(t) {
|
||||
return isBackgroundTask(t) && !(false && isPanelAgentTask(t));
|
||||
}
|
||||
function _temp2(s_0) {
|
||||
return s_0.viewingAgentTaskId;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.tasks;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SummaryPill selected={tasksSelected} onClick={onOpenDialog}>
|
||||
{getPillLabel(runningTasks)}
|
||||
</SummaryPill>
|
||||
{pillNeedsCta(runningTasks) && (
|
||||
<Text dimColor> · {figures.arrowDown} to view</Text>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type AgentPillProps = {
|
||||
name: string;
|
||||
color?: keyof Theme;
|
||||
isSelected: boolean;
|
||||
isViewed: boolean;
|
||||
isIdle: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
function AgentPill(t0) {
|
||||
const $ = _c(19);
|
||||
const {
|
||||
name,
|
||||
color,
|
||||
isSelected,
|
||||
isViewed,
|
||||
isIdle,
|
||||
onClick
|
||||
} = t0;
|
||||
const [hover, setHover] = useState(false);
|
||||
const highlighted = isSelected || hover;
|
||||
let label;
|
||||
name: string
|
||||
color?: keyof Theme
|
||||
isSelected: boolean
|
||||
isViewed: boolean
|
||||
isIdle: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
function AgentPill({
|
||||
name,
|
||||
color,
|
||||
isSelected,
|
||||
isViewed,
|
||||
isIdle,
|
||||
onClick,
|
||||
}: AgentPillProps): React.ReactNode {
|
||||
const [hover, setHover] = useState(false)
|
||||
// Hover mirrors the keyboard-selected look so the affordance is familiar.
|
||||
const highlighted = isSelected || hover
|
||||
|
||||
let label: React.ReactNode
|
||||
if (highlighted) {
|
||||
let t1;
|
||||
if ($[0] !== color || $[1] !== isViewed || $[2] !== name) {
|
||||
t1 = color ? <Text backgroundColor={color} color="inverseText" bold={isViewed}>@{name}</Text> : <Text color="background" inverse={true} bold={isViewed}>@{name}</Text>;
|
||||
$[0] = color;
|
||||
$[1] = isViewed;
|
||||
$[2] = name;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
label = t1;
|
||||
label = color ? (
|
||||
<Text backgroundColor={color} color="inverseText" bold={isViewed}>
|
||||
@{name}
|
||||
</Text>
|
||||
) : (
|
||||
<Text color="background" inverse bold={isViewed}>
|
||||
@{name}
|
||||
</Text>
|
||||
)
|
||||
} else if (isIdle) {
|
||||
label = (
|
||||
<Text dimColor bold={isViewed}>
|
||||
@{name}
|
||||
</Text>
|
||||
)
|
||||
} else if (isViewed) {
|
||||
label = (
|
||||
<Text color={color} bold>
|
||||
@{name}
|
||||
</Text>
|
||||
)
|
||||
} else {
|
||||
if (isIdle) {
|
||||
let t1;
|
||||
if ($[4] !== isViewed || $[5] !== name) {
|
||||
t1 = <Text dimColor={true} bold={isViewed}>@{name}</Text>;
|
||||
$[4] = isViewed;
|
||||
$[5] = name;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
}
|
||||
label = t1;
|
||||
} else {
|
||||
if (isViewed) {
|
||||
let t1;
|
||||
if ($[7] !== color || $[8] !== name) {
|
||||
t1 = <Text color={color} bold={true}>@{name}</Text>;
|
||||
$[7] = color;
|
||||
$[8] = name;
|
||||
$[9] = t1;
|
||||
} else {
|
||||
t1 = $[9];
|
||||
}
|
||||
label = t1;
|
||||
} else {
|
||||
const t1 = !color;
|
||||
let t2;
|
||||
if ($[10] !== color || $[11] !== name || $[12] !== t1) {
|
||||
t2 = <Text color={color} dimColor={t1}>@{name}</Text>;
|
||||
$[10] = color;
|
||||
$[11] = name;
|
||||
$[12] = t1;
|
||||
$[13] = t2;
|
||||
} else {
|
||||
t2 = $[13];
|
||||
}
|
||||
label = t2;
|
||||
}
|
||||
}
|
||||
label = (
|
||||
<Text color={color} dimColor={!color}>
|
||||
@{name}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
if (!onClick) {
|
||||
return label;
|
||||
}
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => setHover(true);
|
||||
t2 = () => setHover(false);
|
||||
$[14] = t1;
|
||||
$[15] = t2;
|
||||
} else {
|
||||
t1 = $[14];
|
||||
t2 = $[15];
|
||||
}
|
||||
let t3;
|
||||
if ($[16] !== label || $[17] !== onClick) {
|
||||
t3 = <Box onClick={onClick} onMouseEnter={t1} onMouseLeave={t2}>{label}</Box>;
|
||||
$[16] = label;
|
||||
$[17] = onClick;
|
||||
$[18] = t3;
|
||||
} else {
|
||||
t3 = $[18];
|
||||
}
|
||||
return t3;
|
||||
|
||||
if (!onClick) return label
|
||||
return (
|
||||
<Box
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
{label}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function SummaryPill(t0) {
|
||||
const $ = _c(8);
|
||||
const {
|
||||
selected,
|
||||
onClick,
|
||||
children
|
||||
} = t0;
|
||||
const [hover, setHover] = useState(false);
|
||||
const t1 = selected || hover;
|
||||
let t2;
|
||||
if ($[0] !== children || $[1] !== t1) {
|
||||
t2 = <Text color="background" inverse={t1}>{children}</Text>;
|
||||
$[0] = children;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const label = t2;
|
||||
if (!onClick) {
|
||||
return label;
|
||||
}
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = () => setHover(true);
|
||||
t4 = () => setHover(false);
|
||||
$[3] = t3;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
t4 = $[4];
|
||||
}
|
||||
let t5;
|
||||
if ($[5] !== label || $[6] !== onClick) {
|
||||
t5 = <Box onClick={onClick} onMouseEnter={t3} onMouseLeave={t4}>{label}</Box>;
|
||||
$[5] = label;
|
||||
$[6] = onClick;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
return t5;
|
||||
|
||||
function SummaryPill({
|
||||
selected,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
selected: boolean
|
||||
onClick?: () => void
|
||||
children: React.ReactNode
|
||||
}): React.ReactNode {
|
||||
const [hover, setHover] = useState(false)
|
||||
const label = (
|
||||
<Text color="background" inverse={selected || hover}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
if (!onClick) return label
|
||||
return (
|
||||
<Box
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
{label}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function getAgentThemeColor(colorName: string | undefined): keyof Theme | undefined {
|
||||
if (!colorName) return undefined;
|
||||
|
||||
function getAgentThemeColor(
|
||||
colorName: string | undefined,
|
||||
): keyof Theme | undefined {
|
||||
if (!colorName) return undefined
|
||||
if (AGENT_COLORS.includes(colorName as AgentColorName)) {
|
||||
return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName];
|
||||
return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]
|
||||
}
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,250 +1,136 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import type { DeepImmutable } from 'src/types/utils.js';
|
||||
import { useElapsedTime } from '../../hooks/useElapsedTime.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import type { DreamTaskState } from '../../tasks/DreamTask/DreamTask.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import { Byline } from '../design-system/Byline.js';
|
||||
import { Dialog } from '../design-system/Dialog.js';
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
||||
import React from 'react'
|
||||
import type { DeepImmutable } from 'src/types/utils.js'
|
||||
import { useElapsedTime } from '../../hooks/useElapsedTime.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import type { DreamTaskState } from '../../tasks/DreamTask/DreamTask.js'
|
||||
import { plural } from '../../utils/stringUtils.js'
|
||||
import { Byline } from '../design-system/Byline.js'
|
||||
import { Dialog } from '../design-system/Dialog.js'
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||
|
||||
type Props = {
|
||||
task: DeepImmutable<DreamTaskState>;
|
||||
onDone: () => void;
|
||||
onBack?: () => void;
|
||||
onKill?: () => void;
|
||||
};
|
||||
task: DeepImmutable<DreamTaskState>
|
||||
onDone: () => void
|
||||
onBack?: () => void
|
||||
onKill?: () => void
|
||||
}
|
||||
|
||||
// How many recent turns to render. Earlier turns collapse to a count.
|
||||
const VISIBLE_TURNS = 6;
|
||||
export function DreamDetailDialog(t0) {
|
||||
const $ = _c(70);
|
||||
const {
|
||||
task,
|
||||
onDone,
|
||||
onBack,
|
||||
onKill
|
||||
} = t0;
|
||||
const elapsedTime = useElapsedTime(task.startTime, task.status === "running", 1000, 0);
|
||||
let t1;
|
||||
if ($[0] !== onDone) {
|
||||
t1 = {
|
||||
"confirm:yes": onDone
|
||||
};
|
||||
$[0] = onDone;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
const VISIBLE_TURNS = 6
|
||||
|
||||
export function DreamDetailDialog({
|
||||
task,
|
||||
onDone,
|
||||
onBack,
|
||||
onKill,
|
||||
}: Props): React.ReactNode {
|
||||
const elapsedTime = useElapsedTime(
|
||||
task.startTime,
|
||||
task.status === 'running',
|
||||
1000,
|
||||
0,
|
||||
)
|
||||
|
||||
// Dialog handles confirm:no (Esc) → onCancel. Wire confirm:yes (Enter/y) too.
|
||||
useKeybindings({ 'confirm:yes': onDone }, { context: 'Confirmation' })
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onDone()
|
||||
} else if (e.key === 'left' && onBack) {
|
||||
e.preventDefault()
|
||||
onBack()
|
||||
} else if (e.key === 'x' && task.status === 'running' && onKill) {
|
||||
e.preventDefault()
|
||||
onKill()
|
||||
}
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
useKeybindings(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== onBack || $[4] !== onDone || $[5] !== onKill || $[6] !== task.status) {
|
||||
t3 = e => {
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
onDone();
|
||||
} else {
|
||||
if (e.key === "left" && onBack) {
|
||||
e.preventDefault();
|
||||
onBack();
|
||||
} else {
|
||||
if (e.key === "x" && task.status === "running" && onKill) {
|
||||
e.preventDefault();
|
||||
onKill();
|
||||
}
|
||||
|
||||
// Turns with text to show. Tool-only turns (text='') are dropped entirely —
|
||||
// the per-turn toolUseCount already captures that work.
|
||||
const visibleTurns = task.turns.filter(t => t.text !== '')
|
||||
const shown = visibleTurns.slice(-VISIBLE_TURNS)
|
||||
const hidden = visibleTurns.length - shown.length
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Dialog
|
||||
title="Memory consolidation"
|
||||
subtitle={
|
||||
<Text dimColor>
|
||||
{elapsedTime} · reviewing {task.sessionsReviewing}{' '}
|
||||
{plural(task.sessionsReviewing, 'session')}
|
||||
{task.filesTouched.length > 0 && (
|
||||
<>
|
||||
{' '}
|
||||
· {task.filesTouched.length}{' '}
|
||||
{plural(task.filesTouched.length, 'file')} touched
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
}
|
||||
}
|
||||
};
|
||||
$[3] = onBack;
|
||||
$[4] = onDone;
|
||||
$[5] = onKill;
|
||||
$[6] = task.status;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
const handleKeyDown = t3;
|
||||
let T0;
|
||||
let T1;
|
||||
let T2;
|
||||
let t10;
|
||||
let t11;
|
||||
let t12;
|
||||
let t13;
|
||||
let t14;
|
||||
let t15;
|
||||
let t16;
|
||||
let t4;
|
||||
let t5;
|
||||
let t6;
|
||||
let t7;
|
||||
let t8;
|
||||
let t9;
|
||||
if ($[8] !== elapsedTime || $[9] !== handleKeyDown || $[10] !== onBack || $[11] !== onDone || $[12] !== onKill || $[13] !== task.filesTouched.length || $[14] !== task.sessionsReviewing || $[15] !== task.status || $[16] !== task.turns) {
|
||||
const visibleTurns = task.turns.filter(_temp);
|
||||
const shown = visibleTurns.slice(-VISIBLE_TURNS);
|
||||
const hidden = visibleTurns.length - shown.length;
|
||||
T2 = Box;
|
||||
t13 = "column";
|
||||
t14 = 0;
|
||||
t15 = true;
|
||||
t16 = handleKeyDown;
|
||||
T1 = Dialog;
|
||||
t8 = "Memory consolidation";
|
||||
const t17 = task.sessionsReviewing;
|
||||
let t18;
|
||||
if ($[33] !== task.sessionsReviewing) {
|
||||
t18 = plural(task.sessionsReviewing, "session");
|
||||
$[33] = task.sessionsReviewing;
|
||||
$[34] = t18;
|
||||
} else {
|
||||
t18 = $[34];
|
||||
}
|
||||
let t19;
|
||||
if ($[35] !== task.filesTouched.length) {
|
||||
t19 = task.filesTouched.length > 0 && <>{" "}· {task.filesTouched.length}{" "}{plural(task.filesTouched.length, "file")} touched</>;
|
||||
$[35] = task.filesTouched.length;
|
||||
$[36] = t19;
|
||||
} else {
|
||||
t19 = $[36];
|
||||
}
|
||||
if ($[37] !== elapsedTime || $[38] !== t18 || $[39] !== t19 || $[40] !== task.sessionsReviewing) {
|
||||
t9 = <Text dimColor={true}>{elapsedTime} · reviewing {t17}{" "}{t18}{t19}</Text>;
|
||||
$[37] = elapsedTime;
|
||||
$[38] = t18;
|
||||
$[39] = t19;
|
||||
$[40] = task.sessionsReviewing;
|
||||
$[41] = t9;
|
||||
} else {
|
||||
t9 = $[41];
|
||||
}
|
||||
t10 = onDone;
|
||||
t11 = "background";
|
||||
if ($[42] !== onBack || $[43] !== onKill || $[44] !== task.status) {
|
||||
t12 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{task.status === "running" && onKill && <KeyboardShortcutHint shortcut="x" action="stop" />}</Byline>;
|
||||
$[42] = onBack;
|
||||
$[43] = onKill;
|
||||
$[44] = task.status;
|
||||
$[45] = t12;
|
||||
} else {
|
||||
t12 = $[45];
|
||||
}
|
||||
T0 = Box;
|
||||
t4 = "column";
|
||||
t5 = 1;
|
||||
let t20;
|
||||
if ($[46] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t20 = <Text bold={true}>Status:</Text>;
|
||||
$[46] = t20;
|
||||
} else {
|
||||
t20 = $[46];
|
||||
}
|
||||
if ($[47] !== task.status) {
|
||||
t6 = <Text>{t20}{" "}{task.status === "running" ? <Text color="background">running</Text> : task.status === "completed" ? <Text color="success">{task.status}</Text> : <Text color="error">{task.status}</Text>}</Text>;
|
||||
$[47] = task.status;
|
||||
$[48] = t6;
|
||||
} else {
|
||||
t6 = $[48];
|
||||
}
|
||||
t7 = shown.length === 0 ? <Text dimColor={true}>{task.status === "running" ? "Starting\u2026" : "(no text output)"}</Text> : <>{hidden > 0 && <Text dimColor={true}>({hidden} earlier {plural(hidden, "turn")})</Text>}{shown.map(_temp2)}</>;
|
||||
$[8] = elapsedTime;
|
||||
$[9] = handleKeyDown;
|
||||
$[10] = onBack;
|
||||
$[11] = onDone;
|
||||
$[12] = onKill;
|
||||
$[13] = task.filesTouched.length;
|
||||
$[14] = task.sessionsReviewing;
|
||||
$[15] = task.status;
|
||||
$[16] = task.turns;
|
||||
$[17] = T0;
|
||||
$[18] = T1;
|
||||
$[19] = T2;
|
||||
$[20] = t10;
|
||||
$[21] = t11;
|
||||
$[22] = t12;
|
||||
$[23] = t13;
|
||||
$[24] = t14;
|
||||
$[25] = t15;
|
||||
$[26] = t16;
|
||||
$[27] = t4;
|
||||
$[28] = t5;
|
||||
$[29] = t6;
|
||||
$[30] = t7;
|
||||
$[31] = t8;
|
||||
$[32] = t9;
|
||||
} else {
|
||||
T0 = $[17];
|
||||
T1 = $[18];
|
||||
T2 = $[19];
|
||||
t10 = $[20];
|
||||
t11 = $[21];
|
||||
t12 = $[22];
|
||||
t13 = $[23];
|
||||
t14 = $[24];
|
||||
t15 = $[25];
|
||||
t16 = $[26];
|
||||
t4 = $[27];
|
||||
t5 = $[28];
|
||||
t6 = $[29];
|
||||
t7 = $[30];
|
||||
t8 = $[31];
|
||||
t9 = $[32];
|
||||
}
|
||||
let t17;
|
||||
if ($[49] !== T0 || $[50] !== t4 || $[51] !== t5 || $[52] !== t6 || $[53] !== t7) {
|
||||
t17 = <T0 flexDirection={t4} gap={t5}>{t6}{t7}</T0>;
|
||||
$[49] = T0;
|
||||
$[50] = t4;
|
||||
$[51] = t5;
|
||||
$[52] = t6;
|
||||
$[53] = t7;
|
||||
$[54] = t17;
|
||||
} else {
|
||||
t17 = $[54];
|
||||
}
|
||||
let t18;
|
||||
if ($[55] !== T1 || $[56] !== t10 || $[57] !== t11 || $[58] !== t12 || $[59] !== t17 || $[60] !== t8 || $[61] !== t9) {
|
||||
t18 = <T1 title={t8} subtitle={t9} onCancel={t10} color={t11} inputGuide={t12}>{t17}</T1>;
|
||||
$[55] = T1;
|
||||
$[56] = t10;
|
||||
$[57] = t11;
|
||||
$[58] = t12;
|
||||
$[59] = t17;
|
||||
$[60] = t8;
|
||||
$[61] = t9;
|
||||
$[62] = t18;
|
||||
} else {
|
||||
t18 = $[62];
|
||||
}
|
||||
let t19;
|
||||
if ($[63] !== T2 || $[64] !== t13 || $[65] !== t14 || $[66] !== t15 || $[67] !== t16 || $[68] !== t18) {
|
||||
t19 = <T2 flexDirection={t13} tabIndex={t14} autoFocus={t15} onKeyDown={t16}>{t18}</T2>;
|
||||
$[63] = T2;
|
||||
$[64] = t13;
|
||||
$[65] = t14;
|
||||
$[66] = t15;
|
||||
$[67] = t16;
|
||||
$[68] = t18;
|
||||
$[69] = t19;
|
||||
} else {
|
||||
t19 = $[69];
|
||||
}
|
||||
return t19;
|
||||
}
|
||||
function _temp2(turn, i) {
|
||||
return <Box key={i} flexDirection="column"><Text wrap="wrap">{turn.text}</Text>{turn.toolUseCount > 0 && <Text dimColor={true}>{" "}({turn.toolUseCount}{" "}{plural(turn.toolUseCount, "tool")})</Text>}</Box>;
|
||||
}
|
||||
function _temp(t) {
|
||||
return t.text !== "";
|
||||
onCancel={onDone}
|
||||
color="background"
|
||||
inputGuide={exitState =>
|
||||
exitState.pending ? (
|
||||
<Text>Press {exitState.keyName} again to exit</Text>
|
||||
) : (
|
||||
<Byline>
|
||||
{onBack && <KeyboardShortcutHint shortcut="←" action="go back" />}
|
||||
<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />
|
||||
{task.status === 'running' && onKill && (
|
||||
<KeyboardShortcutHint shortcut="x" action="stop" />
|
||||
)}
|
||||
</Byline>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>
|
||||
<Text bold>Status:</Text>{' '}
|
||||
{task.status === 'running' ? (
|
||||
<Text color="background">running</Text>
|
||||
) : task.status === 'completed' ? (
|
||||
<Text color="success">{task.status}</Text>
|
||||
) : (
|
||||
<Text color="error">{task.status}</Text>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{shown.length === 0 ? (
|
||||
<Text dimColor>
|
||||
{task.status === 'running' ? 'Starting…' : '(no text output)'}
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
{hidden > 0 && (
|
||||
<Text dimColor>
|
||||
({hidden} earlier {plural(hidden, 'turn')})
|
||||
</Text>
|
||||
)}
|
||||
{shown.map((turn, i) => (
|
||||
<Box key={i} flexDirection="column">
|
||||
<Text wrap="wrap">{turn.text}</Text>
|
||||
{turn.toolUseCount > 0 && (
|
||||
<Text dimColor>
|
||||
{' '}({turn.toolUseCount}{' '}
|
||||
{plural(turn.toolUseCount, 'tool')})
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Dialog>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,265 +1,193 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useMemo } from 'react';
|
||||
import type { DeepImmutable } from 'src/types/utils.js';
|
||||
import { useElapsedTime } from '../../hooks/useElapsedTime.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { Box, Text, useTheme } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import { getEmptyToolPermissionContext } from '../../Tool.js';
|
||||
import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js';
|
||||
import { getTools } from '../../tools.js';
|
||||
import { formatNumber, truncateToWidth } from '../../utils/format.js';
|
||||
import { toInkColor } from '../../utils/ink.js';
|
||||
import { Byline } from '../design-system/Byline.js';
|
||||
import { Dialog } from '../design-system/Dialog.js';
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
||||
import { renderToolActivity } from './renderToolActivity.js';
|
||||
import { describeTeammateActivity } from './taskStatusUtils.js';
|
||||
import React, { useMemo } from 'react'
|
||||
import type { DeepImmutable } from 'src/types/utils.js'
|
||||
import { useElapsedTime } from '../../hooks/useElapsedTime.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { Box, Text, useTheme } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import { getEmptyToolPermissionContext } from '../../Tool.js'
|
||||
import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js'
|
||||
import { getTools } from '../../tools.js'
|
||||
import { formatNumber, truncateToWidth } from '../../utils/format.js'
|
||||
import { toInkColor } from '../../utils/ink.js'
|
||||
import { Byline } from '../design-system/Byline.js'
|
||||
import { Dialog } from '../design-system/Dialog.js'
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||
import { renderToolActivity } from './renderToolActivity.js'
|
||||
import { describeTeammateActivity } from './taskStatusUtils.js'
|
||||
|
||||
type Props = {
|
||||
teammate: DeepImmutable<InProcessTeammateTaskState>;
|
||||
onDone: () => void;
|
||||
onKill?: () => void;
|
||||
onBack?: () => void;
|
||||
onForeground?: () => void;
|
||||
};
|
||||
export function InProcessTeammateDetailDialog(t0) {
|
||||
const $ = _c(63);
|
||||
const {
|
||||
teammate,
|
||||
onDone,
|
||||
onKill,
|
||||
onBack,
|
||||
onForeground
|
||||
} = t0;
|
||||
const [theme] = useTheme();
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = getTools(getEmptyToolPermissionContext());
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const tools = t1;
|
||||
const elapsedTime = useElapsedTime(teammate.startTime, teammate.status === "running", 1000, teammate.totalPausedMs ?? 0);
|
||||
let t2;
|
||||
if ($[1] !== onDone) {
|
||||
t2 = {
|
||||
"confirm:yes": onDone
|
||||
};
|
||||
$[1] = onDone;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
useKeybindings(t2, t3);
|
||||
let t4;
|
||||
if ($[4] !== onBack || $[5] !== onDone || $[6] !== onForeground || $[7] !== onKill || $[8] !== teammate.status) {
|
||||
t4 = e => {
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
onDone();
|
||||
} else {
|
||||
if (e.key === "left" && onBack) {
|
||||
e.preventDefault();
|
||||
onBack();
|
||||
} else {
|
||||
if (e.key === "x" && teammate.status === "running" && onKill) {
|
||||
e.preventDefault();
|
||||
onKill();
|
||||
} else {
|
||||
if (e.key === "f" && teammate.status === "running" && onForeground) {
|
||||
e.preventDefault();
|
||||
onForeground();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
$[4] = onBack;
|
||||
$[5] = onDone;
|
||||
$[6] = onForeground;
|
||||
$[7] = onKill;
|
||||
$[8] = teammate.status;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
const handleKeyDown = t4;
|
||||
let t5;
|
||||
if ($[10] !== teammate) {
|
||||
t5 = describeTeammateActivity(teammate);
|
||||
$[10] = teammate;
|
||||
$[11] = t5;
|
||||
} else {
|
||||
t5 = $[11];
|
||||
}
|
||||
const activity = t5;
|
||||
const tokenCount = teammate.result?.totalTokens ?? teammate.progress?.tokenCount;
|
||||
const toolUseCount = teammate.result?.totalToolUseCount ?? teammate.progress?.toolUseCount;
|
||||
let t6;
|
||||
if ($[12] !== teammate.prompt) {
|
||||
t6 = truncateToWidth(teammate.prompt, 300);
|
||||
$[12] = teammate.prompt;
|
||||
$[13] = t6;
|
||||
} else {
|
||||
t6 = $[13];
|
||||
}
|
||||
const displayPrompt = t6;
|
||||
let t7;
|
||||
if ($[14] !== teammate.identity.color) {
|
||||
t7 = toInkColor(teammate.identity.color);
|
||||
$[14] = teammate.identity.color;
|
||||
$[15] = t7;
|
||||
} else {
|
||||
t7 = $[15];
|
||||
}
|
||||
let t8;
|
||||
if ($[16] !== t7 || $[17] !== teammate.identity.agentName) {
|
||||
t8 = <Text color={t7}>@{teammate.identity.agentName}</Text>;
|
||||
$[16] = t7;
|
||||
$[17] = teammate.identity.agentName;
|
||||
$[18] = t8;
|
||||
} else {
|
||||
t8 = $[18];
|
||||
}
|
||||
let t9;
|
||||
if ($[19] !== activity) {
|
||||
t9 = activity && <Text dimColor={true}> ({activity})</Text>;
|
||||
$[19] = activity;
|
||||
$[20] = t9;
|
||||
} else {
|
||||
t9 = $[20];
|
||||
}
|
||||
let t10;
|
||||
if ($[21] !== t8 || $[22] !== t9) {
|
||||
t10 = <Text>{t8}{t9}</Text>;
|
||||
$[21] = t8;
|
||||
$[22] = t9;
|
||||
$[23] = t10;
|
||||
} else {
|
||||
t10 = $[23];
|
||||
}
|
||||
const title = t10;
|
||||
let t11;
|
||||
if ($[24] !== teammate.status) {
|
||||
t11 = teammate.status !== "running" && <Text color={teammate.status === "completed" ? "success" : teammate.status === "killed" ? "warning" : "error"}>{teammate.status === "completed" ? "Completed" : teammate.status === "failed" ? "Failed" : "Stopped"}{" \xB7 "}</Text>;
|
||||
$[24] = teammate.status;
|
||||
$[25] = t11;
|
||||
} else {
|
||||
t11 = $[25];
|
||||
}
|
||||
let t12;
|
||||
if ($[26] !== tokenCount) {
|
||||
t12 = tokenCount !== undefined && tokenCount > 0 && <> · {formatNumber(tokenCount)} tokens</>;
|
||||
$[26] = tokenCount;
|
||||
$[27] = t12;
|
||||
} else {
|
||||
t12 = $[27];
|
||||
}
|
||||
let t13;
|
||||
if ($[28] !== toolUseCount) {
|
||||
t13 = toolUseCount !== undefined && toolUseCount > 0 && <>{" "}· {toolUseCount} {toolUseCount === 1 ? "tool" : "tools"}</>;
|
||||
$[28] = toolUseCount;
|
||||
$[29] = t13;
|
||||
} else {
|
||||
t13 = $[29];
|
||||
}
|
||||
let t14;
|
||||
if ($[30] !== elapsedTime || $[31] !== t12 || $[32] !== t13) {
|
||||
t14 = <Text dimColor={true}>{elapsedTime}{t12}{t13}</Text>;
|
||||
$[30] = elapsedTime;
|
||||
$[31] = t12;
|
||||
$[32] = t13;
|
||||
$[33] = t14;
|
||||
} else {
|
||||
t14 = $[33];
|
||||
}
|
||||
let t15;
|
||||
if ($[34] !== t11 || $[35] !== t14) {
|
||||
t15 = <Text>{t11}{t14}</Text>;
|
||||
$[34] = t11;
|
||||
$[35] = t14;
|
||||
$[36] = t15;
|
||||
} else {
|
||||
t15 = $[36];
|
||||
}
|
||||
const subtitle = t15;
|
||||
let t16;
|
||||
if ($[37] !== onBack || $[38] !== onForeground || $[39] !== onKill || $[40] !== teammate.status) {
|
||||
t16 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{teammate.status === "running" && onKill && <KeyboardShortcutHint shortcut="x" action="stop" />}{teammate.status === "running" && onForeground && <KeyboardShortcutHint shortcut="f" action="foreground" />}</Byline>;
|
||||
$[37] = onBack;
|
||||
$[38] = onForeground;
|
||||
$[39] = onKill;
|
||||
$[40] = teammate.status;
|
||||
$[41] = t16;
|
||||
} else {
|
||||
t16 = $[41];
|
||||
}
|
||||
let t17;
|
||||
if ($[42] !== teammate.progress || $[43] !== teammate.status || $[44] !== theme) {
|
||||
t17 = teammate.status === "running" && teammate.progress?.recentActivities && teammate.progress.recentActivities.length > 0 && <Box flexDirection="column"><Text bold={true} dimColor={true}>Progress</Text>{teammate.progress.recentActivities.map((activity_0, i) => <Text key={i} dimColor={i < teammate.progress.recentActivities.length - 1} wrap="truncate-end">{i === teammate.progress.recentActivities.length - 1 ? "\u203A " : " "}{renderToolActivity(activity_0, tools, theme)}</Text>)}</Box>;
|
||||
$[42] = teammate.progress;
|
||||
$[43] = teammate.status;
|
||||
$[44] = theme;
|
||||
$[45] = t17;
|
||||
} else {
|
||||
t17 = $[45];
|
||||
}
|
||||
let t18;
|
||||
if ($[46] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t18 = <Text bold={true} dimColor={true}>Prompt</Text>;
|
||||
$[46] = t18;
|
||||
} else {
|
||||
t18 = $[46];
|
||||
}
|
||||
let t19;
|
||||
if ($[47] !== displayPrompt) {
|
||||
t19 = <Box flexDirection="column" marginTop={1}>{t18}<Text wrap="wrap">{displayPrompt}</Text></Box>;
|
||||
$[47] = displayPrompt;
|
||||
$[48] = t19;
|
||||
} else {
|
||||
t19 = $[48];
|
||||
}
|
||||
let t20;
|
||||
if ($[49] !== teammate.error || $[50] !== teammate.status) {
|
||||
t20 = teammate.status === "failed" && teammate.error && <Box flexDirection="column" marginTop={1}><Text bold={true} color="error">Error</Text><Text color="error" wrap="wrap">{teammate.error}</Text></Box>;
|
||||
$[49] = teammate.error;
|
||||
$[50] = teammate.status;
|
||||
$[51] = t20;
|
||||
} else {
|
||||
t20 = $[51];
|
||||
}
|
||||
let t21;
|
||||
if ($[52] !== onDone || $[53] !== subtitle || $[54] !== t16 || $[55] !== t17 || $[56] !== t19 || $[57] !== t20 || $[58] !== title) {
|
||||
t21 = <Dialog title={title} subtitle={subtitle} onCancel={onDone} color="background" inputGuide={t16}>{t17}{t19}{t20}</Dialog>;
|
||||
$[52] = onDone;
|
||||
$[53] = subtitle;
|
||||
$[54] = t16;
|
||||
$[55] = t17;
|
||||
$[56] = t19;
|
||||
$[57] = t20;
|
||||
$[58] = title;
|
||||
$[59] = t21;
|
||||
} else {
|
||||
t21 = $[59];
|
||||
}
|
||||
let t22;
|
||||
if ($[60] !== handleKeyDown || $[61] !== t21) {
|
||||
t22 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t21}</Box>;
|
||||
$[60] = handleKeyDown;
|
||||
$[61] = t21;
|
||||
$[62] = t22;
|
||||
} else {
|
||||
t22 = $[62];
|
||||
}
|
||||
return t22;
|
||||
teammate: DeepImmutable<InProcessTeammateTaskState>
|
||||
onDone: () => void
|
||||
onKill?: () => void
|
||||
onBack?: () => void
|
||||
onForeground?: () => void
|
||||
}
|
||||
export function InProcessTeammateDetailDialog({
|
||||
teammate,
|
||||
onDone,
|
||||
onKill,
|
||||
onBack,
|
||||
onForeground,
|
||||
}: Props): React.ReactNode {
|
||||
const [theme] = useTheme()
|
||||
const tools = useMemo(() => getTools(getEmptyToolPermissionContext()), [])
|
||||
|
||||
const elapsedTime = useElapsedTime(
|
||||
teammate.startTime,
|
||||
teammate.status === 'running',
|
||||
1000,
|
||||
teammate.totalPausedMs ?? 0,
|
||||
)
|
||||
|
||||
// Restore confirm:yes (Enter/y) dismissal — Dialog handles confirm:no (Esc)
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:yes': onDone,
|
||||
},
|
||||
{ context: 'Confirmation' },
|
||||
)
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onDone()
|
||||
} else if (e.key === 'left' && onBack) {
|
||||
e.preventDefault()
|
||||
onBack()
|
||||
} else if (e.key === 'x' && teammate.status === 'running' && onKill) {
|
||||
e.preventDefault()
|
||||
onKill()
|
||||
} else if (e.key === 'f' && teammate.status === 'running' && onForeground) {
|
||||
e.preventDefault()
|
||||
onForeground()
|
||||
}
|
||||
}
|
||||
|
||||
const activity = describeTeammateActivity(teammate)
|
||||
|
||||
const tokenCount =
|
||||
teammate.result?.totalTokens ?? teammate.progress?.tokenCount
|
||||
const toolUseCount =
|
||||
teammate.result?.totalToolUseCount ?? teammate.progress?.toolUseCount
|
||||
|
||||
const displayPrompt = truncateToWidth(teammate.prompt, 300)
|
||||
|
||||
const title = (
|
||||
<Text>
|
||||
<Text color={toInkColor(teammate.identity.color)}>
|
||||
@{teammate.identity.agentName}
|
||||
</Text>
|
||||
{activity && <Text dimColor> ({activity})</Text>}
|
||||
</Text>
|
||||
)
|
||||
|
||||
const subtitle = (
|
||||
<Text>
|
||||
{teammate.status !== 'running' && (
|
||||
<Text
|
||||
color={
|
||||
teammate.status === 'completed'
|
||||
? 'success'
|
||||
: teammate.status === 'killed'
|
||||
? 'warning'
|
||||
: 'error'
|
||||
}
|
||||
>
|
||||
{teammate.status === 'completed'
|
||||
? 'Completed'
|
||||
: teammate.status === 'failed'
|
||||
? 'Failed'
|
||||
: 'Stopped'}
|
||||
{' · '}
|
||||
</Text>
|
||||
)}
|
||||
<Text dimColor>
|
||||
{elapsedTime}
|
||||
{tokenCount !== undefined && tokenCount > 0 && (
|
||||
<> · {formatNumber(tokenCount)} tokens</>
|
||||
)}
|
||||
{toolUseCount !== undefined && toolUseCount > 0 && (
|
||||
<>
|
||||
{' '}
|
||||
· {toolUseCount} {toolUseCount === 1 ? 'tool' : 'tools'}
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</Text>
|
||||
)
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Dialog
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
onCancel={onDone}
|
||||
color="background"
|
||||
inputGuide={exitState =>
|
||||
exitState.pending ? (
|
||||
<Text>Press {exitState.keyName} again to exit</Text>
|
||||
) : (
|
||||
<Byline>
|
||||
{onBack && <KeyboardShortcutHint shortcut="←" action="go back" />}
|
||||
<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />
|
||||
{teammate.status === 'running' && onKill && (
|
||||
<KeyboardShortcutHint shortcut="x" action="stop" />
|
||||
)}
|
||||
{teammate.status === 'running' && onForeground && (
|
||||
<KeyboardShortcutHint shortcut="f" action="foreground" />
|
||||
)}
|
||||
</Byline>
|
||||
)
|
||||
}
|
||||
>
|
||||
{/* Recent activities for running teammates */}
|
||||
{teammate.status === 'running' &&
|
||||
teammate.progress?.recentActivities &&
|
||||
teammate.progress.recentActivities.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
<Text bold dimColor>
|
||||
Progress
|
||||
</Text>
|
||||
{teammate.progress.recentActivities.map((activity, i) => (
|
||||
<Text
|
||||
key={i}
|
||||
dimColor={i < teammate.progress!.recentActivities!.length - 1}
|
||||
wrap="truncate-end"
|
||||
>
|
||||
{i === teammate.progress!.recentActivities!.length - 1
|
||||
? '› '
|
||||
: ' '}
|
||||
{renderToolActivity(activity, tools, theme)}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Prompt section */}
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text bold dimColor>
|
||||
Prompt
|
||||
</Text>
|
||||
<Text wrap="wrap">{displayPrompt}</Text>
|
||||
</Box>
|
||||
|
||||
{/* Error details if failed */}
|
||||
{teammate.status === 'failed' && teammate.error && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text bold color="error">
|
||||
Error
|
||||
</Text>
|
||||
<Text color="error" wrap="wrap">
|
||||
{teammate.error}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Dialog>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,17 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useRef } from 'react';
|
||||
import type { RemoteAgentTaskState } from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js';
|
||||
import type { DeepImmutable } from 'src/types/utils.js';
|
||||
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js';
|
||||
import { useSettings } from '../../hooks/useSettings.js';
|
||||
import { Text, useAnimationFrame } from '../../ink.js';
|
||||
import { count } from '../../utils/array.js';
|
||||
import { getRainbowColor } from '../../utils/thinking.js';
|
||||
const TICK_MS = 80;
|
||||
type ReviewStage = NonNullable<NonNullable<RemoteAgentTaskState['reviewProgress']>['stage']>;
|
||||
import React, { useRef } from 'react'
|
||||
import type { RemoteAgentTaskState } from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js'
|
||||
import type { DeepImmutable } from 'src/types/utils.js'
|
||||
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'
|
||||
import { useSettings } from '../../hooks/useSettings.js'
|
||||
import { Text, useAnimationFrame } from '../../ink.js'
|
||||
import { count } from '../../utils/array.js'
|
||||
import { getRainbowColor } from '../../utils/thinking.js'
|
||||
|
||||
const TICK_MS = 80
|
||||
|
||||
type ReviewStage = NonNullable<
|
||||
NonNullable<RemoteAgentTaskState['reviewProgress']>['stage']
|
||||
>
|
||||
|
||||
/**
|
||||
* Stage-appropriate counts line for a running review. Shared between the
|
||||
@@ -19,52 +22,48 @@ type ReviewStage = NonNullable<NonNullable<RemoteAgentTaskState['reviewProgress'
|
||||
* Canonical behavior: word labels (not ✓/✗), hide refuted when 0, "deduping"
|
||||
* for the synthesizing stage (matches STAGE_LABELS in the detail dialog).
|
||||
*/
|
||||
export function formatReviewStageCounts(stage: ReviewStage | undefined, found: number, verified: number, refuted: number): string {
|
||||
export function formatReviewStageCounts(
|
||||
stage: ReviewStage | undefined,
|
||||
found: number,
|
||||
verified: number,
|
||||
refuted: number,
|
||||
): string {
|
||||
// Pre-stage orchestrator images don't write the stage field.
|
||||
if (!stage) return `${found} found · ${verified} verified`;
|
||||
if (!stage) return `${found} found · ${verified} verified`
|
||||
if (stage === 'synthesizing') {
|
||||
const parts = [`${verified} verified`];
|
||||
if (refuted > 0) parts.push(`${refuted} refuted`);
|
||||
parts.push('deduping');
|
||||
return parts.join(' · ');
|
||||
const parts = [`${verified} verified`]
|
||||
if (refuted > 0) parts.push(`${refuted} refuted`)
|
||||
parts.push('deduping')
|
||||
return parts.join(' · ')
|
||||
}
|
||||
if (stage === 'verifying') {
|
||||
const parts = [`${found} found`, `${verified} verified`];
|
||||
if (refuted > 0) parts.push(`${refuted} refuted`);
|
||||
return parts.join(' · ');
|
||||
const parts = [`${found} found`, `${verified} verified`]
|
||||
if (refuted > 0) parts.push(`${refuted} refuted`)
|
||||
return parts.join(' · ')
|
||||
}
|
||||
// stage === 'finding'
|
||||
return found > 0 ? `${found} found` : 'finding';
|
||||
return found > 0 ? `${found} found` : 'finding'
|
||||
}
|
||||
|
||||
// Per-character rainbow gradient, same treatment as the ultraplan keyword.
|
||||
// The phase offset lets the gradient cycle — so the colors sweep along the
|
||||
// text on each animation frame instead of being static.
|
||||
function RainbowText(t0) {
|
||||
const $ = _c(5);
|
||||
const {
|
||||
text,
|
||||
phase: t1
|
||||
} = t0;
|
||||
const phase = t1 === undefined ? 0 : t1;
|
||||
let t2;
|
||||
if ($[0] !== text) {
|
||||
t2 = [...text];
|
||||
$[0] = text;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
let t3;
|
||||
if ($[2] !== phase || $[3] !== t2) {
|
||||
t3 = <>{t2.map((ch, i) => <Text key={i} color={getRainbowColor(i + phase)}>{ch}</Text>)}</>;
|
||||
$[2] = phase;
|
||||
$[3] = t2;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
function RainbowText({
|
||||
text,
|
||||
phase = 0,
|
||||
}: {
|
||||
text: string
|
||||
phase?: number
|
||||
}): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
{[...text].map((ch, i) => (
|
||||
<Text key={i} color={getRainbowColor(i + phase)}>
|
||||
{ch}
|
||||
</Text>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Smooth-tick a count toward target, +1 per frame. Same pattern as the
|
||||
@@ -74,169 +73,129 @@ function RainbowText(t0) {
|
||||
// the clock is frozen), bypass the tick and jump straight to target —
|
||||
// otherwise a frozen `time` would leave the ref stuck at its init value.
|
||||
function useSmoothCount(target: number, time: number, snap: boolean): number {
|
||||
const displayed = useRef(target);
|
||||
const lastTick = useRef(time);
|
||||
const displayed = useRef(target)
|
||||
const lastTick = useRef(time)
|
||||
if (snap || target < displayed.current) {
|
||||
displayed.current = target;
|
||||
displayed.current = target
|
||||
} else if (target > displayed.current && time !== lastTick.current) {
|
||||
displayed.current += 1;
|
||||
lastTick.current = time;
|
||||
displayed.current += 1
|
||||
lastTick.current = time
|
||||
}
|
||||
return displayed.current;
|
||||
return displayed.current
|
||||
}
|
||||
function ReviewRainbowLine(t0) {
|
||||
const $ = _c(15);
|
||||
const {
|
||||
session
|
||||
} = t0;
|
||||
const settings = useSettings();
|
||||
const reducedMotion = settings.prefersReducedMotion ?? false;
|
||||
const p = session.reviewProgress;
|
||||
const running = session.status === "running";
|
||||
const [, time] = useAnimationFrame(running && !reducedMotion ? TICK_MS : null);
|
||||
const targetFound = p?.bugsFound ?? 0;
|
||||
const targetVerified = p?.bugsVerified ?? 0;
|
||||
const targetRefuted = p?.bugsRefuted ?? 0;
|
||||
const snap = reducedMotion || !running;
|
||||
const found = useSmoothCount(targetFound, time, snap);
|
||||
const verified = useSmoothCount(targetVerified, time, snap);
|
||||
const refuted = useSmoothCount(targetRefuted, time, snap);
|
||||
const phase = Math.floor(time / (TICK_MS * 3)) % 7;
|
||||
if (session.status === "completed") {
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <><Text color="background">{DIAMOND_FILLED} </Text><RainbowText text="ultrareview" phase={0} /><Text dimColor={true}> ready · shift+↓ to view</Text></>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
return t1;
|
||||
|
||||
function ReviewRainbowLine({
|
||||
session,
|
||||
}: {
|
||||
session: DeepImmutable<RemoteAgentTaskState>
|
||||
}): React.ReactNode {
|
||||
const settings = useSettings()
|
||||
const reducedMotion = settings.prefersReducedMotion ?? false
|
||||
const p = session.reviewProgress
|
||||
const running = session.status === 'running'
|
||||
// Animation clock runs only while running — completed/failed are static.
|
||||
// Disabled entirely when the user prefers reduced motion.
|
||||
//
|
||||
// The ref is intentionally discarded: this component is rendered inside
|
||||
// <Text> wrappers (BackgroundTasksDialog, RemoteSessionDetailDialog), and
|
||||
// Ink can't nest <Box> inside <Text>. Dropping the ref means
|
||||
// useTerminalViewport's isVisible stays true, so the clock ticks even when
|
||||
// scrolled off-screen — acceptable for a single 30-char line.
|
||||
const [, time] = useAnimationFrame(running && !reducedMotion ? TICK_MS : null)
|
||||
|
||||
const targetFound = p?.bugsFound ?? 0
|
||||
const targetVerified = p?.bugsVerified ?? 0
|
||||
const targetRefuted = p?.bugsRefuted ?? 0
|
||||
// snap when the clock isn't advancing (reduced motion, or not running) —
|
||||
// useAnimationFrame(null) freezes `time` at its mount value, which would
|
||||
// leave the tick-gate permanently false.
|
||||
const snap = reducedMotion || !running
|
||||
const found = useSmoothCount(targetFound, time, snap)
|
||||
const verified = useSmoothCount(targetVerified, time, snap)
|
||||
const refuted = useSmoothCount(targetRefuted, time, snap)
|
||||
|
||||
// Phase advances every 3 ticks so the gradient sweep is visible but
|
||||
// not frantic. Modulo keeps it in the 7-color cycle.
|
||||
const phase = Math.floor(time / (TICK_MS * 3)) % 7
|
||||
|
||||
// ◇ open diamond while running (teal, matches cloud-session accent), ◆
|
||||
// filled when terminal. Rainbow is scoped to the word `ultrareview` only —
|
||||
// per design feedback, "there is a limit to the glittering rainbow".
|
||||
// Counts stay dimColor.
|
||||
if (session.status === 'completed') {
|
||||
return (
|
||||
<>
|
||||
<Text color="background">{DIAMOND_FILLED} </Text>
|
||||
<RainbowText text="ultrareview" phase={0} />
|
||||
<Text dimColor> ready · shift+↓ to view</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
if (session.status === "failed") {
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <><Text color="background">{DIAMOND_FILLED} </Text><RainbowText text="ultrareview" phase={0} /><Text color="error" dimColor={true}>{" \xB7 "}error</Text></>;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
if (session.status === 'failed') {
|
||||
return (
|
||||
<>
|
||||
<Text color="background">{DIAMOND_FILLED} </Text>
|
||||
<RainbowText text="ultrareview" phase={0} />
|
||||
<Text color="error" dimColor>
|
||||
{' · '}
|
||||
error
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
let t1;
|
||||
if ($[2] !== found || $[3] !== p || $[4] !== refuted || $[5] !== verified) {
|
||||
t1 = !p ? "setting up" : formatReviewStageCounts(p.stage, found, verified, refuted);
|
||||
$[2] = found;
|
||||
$[3] = p;
|
||||
$[4] = refuted;
|
||||
$[5] = verified;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
}
|
||||
const tail = t1;
|
||||
let t2;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text color="background">{DIAMOND_OPEN} </Text>;
|
||||
$[7] = t2;
|
||||
} else {
|
||||
t2 = $[7];
|
||||
}
|
||||
const t3 = running ? phase : 0;
|
||||
let t4;
|
||||
if ($[8] !== t3) {
|
||||
t4 = <RainbowText text="ultrareview" phase={t3} />;
|
||||
$[8] = t3;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== tail) {
|
||||
t5 = <Text dimColor={true}> · {tail}</Text>;
|
||||
$[10] = tail;
|
||||
$[11] = t5;
|
||||
} else {
|
||||
t5 = $[11];
|
||||
}
|
||||
let t6;
|
||||
if ($[12] !== t4 || $[13] !== t5) {
|
||||
t6 = <>{t2}{t4}{t5}</>;
|
||||
$[12] = t4;
|
||||
$[13] = t5;
|
||||
$[14] = t6;
|
||||
} else {
|
||||
t6 = $[14];
|
||||
}
|
||||
return t6;
|
||||
|
||||
// The !p branch ("setting up") covers the window before the orchestrator
|
||||
// writes its first progress snapshot — container boot + repo clone can
|
||||
// take 1-3 min, during which "0 found" looked hung.
|
||||
const tail = !p
|
||||
? 'setting up'
|
||||
: formatReviewStageCounts(p.stage, found, verified, refuted)
|
||||
return (
|
||||
<>
|
||||
<Text color="background">{DIAMOND_OPEN} </Text>
|
||||
<RainbowText text="ultrareview" phase={running ? phase : 0} />
|
||||
<Text dimColor> · {tail}</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export function RemoteSessionProgress(t0) {
|
||||
const $ = _c(11);
|
||||
const {
|
||||
session
|
||||
} = t0;
|
||||
|
||||
export function RemoteSessionProgress({
|
||||
session,
|
||||
}: {
|
||||
session: DeepImmutable<RemoteAgentTaskState>
|
||||
}): React.ReactNode {
|
||||
// Lite-review: rainbow gradient over the full line, ultraplan-style.
|
||||
// BackgroundTask.tsx delegates the whole <Text> wrapper here so the
|
||||
// gradient spans the title, not just the trailing status.
|
||||
if (session.isRemoteReview) {
|
||||
let t1;
|
||||
if ($[0] !== session) {
|
||||
t1 = <ReviewRainbowLine session={session} />;
|
||||
$[0] = session;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
return <ReviewRainbowLine session={session} />
|
||||
}
|
||||
if (session.status === "completed") {
|
||||
let t1;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text bold={true} color="success" dimColor={true}>done</Text>;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
|
||||
if (session.status === 'completed') {
|
||||
return (
|
||||
<Text bold color="success" dimColor>
|
||||
done
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
if (session.status === "failed") {
|
||||
let t1;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text bold={true} color="error" dimColor={true}>error</Text>;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
|
||||
if (session.status === 'failed') {
|
||||
return (
|
||||
<Text bold color="error" dimColor>
|
||||
error
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
if (!session.todoList.length) {
|
||||
let t1;
|
||||
if ($[4] !== session.status) {
|
||||
t1 = <Text dimColor={true}>{session.status}…</Text>;
|
||||
$[4] = session.status;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
return <Text dimColor>{session.status}…</Text>
|
||||
}
|
||||
let t1;
|
||||
if ($[6] !== session.todoList) {
|
||||
t1 = count(session.todoList, _temp);
|
||||
$[6] = session.todoList;
|
||||
$[7] = t1;
|
||||
} else {
|
||||
t1 = $[7];
|
||||
}
|
||||
const completed = t1;
|
||||
const total = session.todoList.length;
|
||||
let t2;
|
||||
if ($[8] !== completed || $[9] !== total) {
|
||||
t2 = <Text dimColor={true}>{completed}/{total}</Text>;
|
||||
$[8] = completed;
|
||||
$[9] = total;
|
||||
$[10] = t2;
|
||||
} else {
|
||||
t2 = $[10];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
function _temp(_) {
|
||||
return _.status === "completed";
|
||||
|
||||
const completed = count(session.todoList, _ => _.status === 'completed')
|
||||
const total = session.todoList.length
|
||||
return (
|
||||
<Text dimColor>
|
||||
{completed}/{total}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,403 +1,247 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { Suspense, use, useDeferredValue, useEffect, useState } from 'react';
|
||||
import type { DeepImmutable } from 'src/types/utils.js';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import type { LocalShellTaskState } from '../../tasks/LocalShellTask/guards.js';
|
||||
import { formatDuration, formatFileSize, truncateToWidth } from '../../utils/format.js';
|
||||
import { tailFile } from '../../utils/fsOperations.js';
|
||||
import { getTaskOutputPath } from '../../utils/task/diskOutput.js';
|
||||
import { Byline } from '../design-system/Byline.js';
|
||||
import { Dialog } from '../design-system/Dialog.js';
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
||||
import React, {
|
||||
Suspense,
|
||||
use,
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { DeepImmutable } from 'src/types/utils.js'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import type { LocalShellTaskState } from '../../tasks/LocalShellTask/guards.js'
|
||||
import {
|
||||
formatDuration,
|
||||
formatFileSize,
|
||||
truncateToWidth,
|
||||
} from '../../utils/format.js'
|
||||
import { tailFile } from '../../utils/fsOperations.js'
|
||||
import { getTaskOutputPath } from '../../utils/task/diskOutput.js'
|
||||
import { Byline } from '../design-system/Byline.js'
|
||||
import { Dialog } from '../design-system/Dialog.js'
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||
|
||||
type Props = {
|
||||
shell: DeepImmutable<LocalShellTaskState>;
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
onKillShell?: () => void;
|
||||
onBack?: () => void;
|
||||
};
|
||||
const SHELL_DETAIL_TAIL_BYTES = 8192;
|
||||
shell: DeepImmutable<LocalShellTaskState>
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
onKillShell?: () => void
|
||||
onBack?: () => void
|
||||
}
|
||||
|
||||
const SHELL_DETAIL_TAIL_BYTES = 8192
|
||||
|
||||
type TaskOutputResult = {
|
||||
content: string;
|
||||
bytesTotal: number;
|
||||
};
|
||||
content: string
|
||||
bytesTotal: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the tail of the task output file. Only reads the last few KB,
|
||||
* not the entire file.
|
||||
*/
|
||||
async function getTaskOutput(shell: DeepImmutable<LocalShellTaskState>): Promise<TaskOutputResult> {
|
||||
const path = getTaskOutputPath(shell.id);
|
||||
async function getTaskOutput(
|
||||
shell: DeepImmutable<LocalShellTaskState>,
|
||||
): Promise<TaskOutputResult> {
|
||||
const path = getTaskOutputPath(shell.id)
|
||||
try {
|
||||
const result = await tailFile(path, SHELL_DETAIL_TAIL_BYTES);
|
||||
return {
|
||||
content: result.content,
|
||||
bytesTotal: result.bytesTotal
|
||||
};
|
||||
const result = await tailFile(path, SHELL_DETAIL_TAIL_BYTES)
|
||||
return { content: result.content, bytesTotal: result.bytesTotal }
|
||||
} catch {
|
||||
return {
|
||||
content: '',
|
||||
bytesTotal: 0
|
||||
};
|
||||
return { content: '', bytesTotal: 0 }
|
||||
}
|
||||
}
|
||||
export function ShellDetailDialog(t0) {
|
||||
const $ = _c(57);
|
||||
const {
|
||||
shell,
|
||||
onDone,
|
||||
onKillShell,
|
||||
onBack
|
||||
} = t0;
|
||||
const {
|
||||
columns
|
||||
} = useTerminalSize();
|
||||
let t1;
|
||||
if ($[0] !== shell) {
|
||||
t1 = () => getTaskOutput(shell);
|
||||
$[0] = shell;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
|
||||
export function ShellDetailDialog({
|
||||
shell,
|
||||
onDone,
|
||||
onKillShell,
|
||||
onBack,
|
||||
}: Props): React.ReactNode {
|
||||
const { columns } = useTerminalSize()
|
||||
|
||||
// Promise created in initializer (not during render). For running shells,
|
||||
// the effect timer replaces it periodically to pick up new output.
|
||||
// useDeferredValue keeps showing the previous output while the new promise
|
||||
// resolves, preventing the Suspense fallback from flickering.
|
||||
const [outputPromise, setOutputPromise] = useState<Promise<TaskOutputResult>>(
|
||||
() => getTaskOutput(shell),
|
||||
)
|
||||
const deferredOutputPromise = useDeferredValue(outputPromise)
|
||||
|
||||
useEffect(() => {
|
||||
if (shell.status !== 'running') {
|
||||
return
|
||||
}
|
||||
const timer = setInterval(
|
||||
(setOutputPromise, shell) => setOutputPromise(getTaskOutput(shell)),
|
||||
1000,
|
||||
setOutputPromise,
|
||||
shell,
|
||||
)
|
||||
return () => clearInterval(timer)
|
||||
}, [shell.id, shell.status])
|
||||
|
||||
// Handle standard close action
|
||||
const handleClose = () =>
|
||||
onDone('Shell details dismissed', { display: 'system' })
|
||||
|
||||
// Handle additional close actions beyond Dialog's built-in Esc handler
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:yes': handleClose,
|
||||
},
|
||||
{ context: 'Confirmation' },
|
||||
)
|
||||
|
||||
// Handle dialog-specific keys
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onDone('Shell details dismissed', { display: 'system' })
|
||||
} else if (e.key === 'left' && onBack) {
|
||||
e.preventDefault()
|
||||
onBack()
|
||||
} else if (e.key === 'x' && shell.status === 'running' && onKillShell) {
|
||||
e.preventDefault()
|
||||
onKillShell()
|
||||
}
|
||||
}
|
||||
const [outputPromise, setOutputPromise] = useState(t1);
|
||||
const deferredOutputPromise = useDeferredValue(outputPromise);
|
||||
let t2;
|
||||
if ($[2] !== shell) {
|
||||
t2 = () => {
|
||||
if (shell.status !== "running") {
|
||||
return;
|
||||
}
|
||||
const timer = setInterval(_temp, 1000, setOutputPromise, shell);
|
||||
return () => clearInterval(timer);
|
||||
};
|
||||
$[2] = shell;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
let t3;
|
||||
if ($[4] !== shell.id || $[5] !== shell.status) {
|
||||
t3 = [shell.id, shell.status];
|
||||
$[4] = shell.id;
|
||||
$[5] = shell.status;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[7] !== onDone) {
|
||||
t4 = () => onDone("Shell details dismissed", {
|
||||
display: "system"
|
||||
});
|
||||
$[7] = onDone;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
const handleClose = t4;
|
||||
let t5;
|
||||
if ($[9] !== handleClose) {
|
||||
t5 = {
|
||||
"confirm:yes": handleClose
|
||||
};
|
||||
$[9] = handleClose;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
let t6;
|
||||
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
useKeybindings(t5, t6);
|
||||
let t7;
|
||||
if ($[12] !== onBack || $[13] !== onDone || $[14] !== onKillShell || $[15] !== shell.status) {
|
||||
t7 = e => {
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
onDone("Shell details dismissed", {
|
||||
display: "system"
|
||||
});
|
||||
} else {
|
||||
if (e.key === "left" && onBack) {
|
||||
e.preventDefault();
|
||||
onBack();
|
||||
} else {
|
||||
if (e.key === "x" && shell.status === "running" && onKillShell) {
|
||||
e.preventDefault();
|
||||
onKillShell();
|
||||
}
|
||||
|
||||
// Truncate command if too long (for display purposes)
|
||||
const isMonitor = shell.kind === 'monitor'
|
||||
const displayCommand = truncateToWidth(shell.command, 280)
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Dialog
|
||||
title={isMonitor ? 'Monitor details' : 'Shell details'}
|
||||
onCancel={handleClose}
|
||||
color="background"
|
||||
inputGuide={exitState =>
|
||||
exitState.pending ? (
|
||||
<Text>Press {exitState.keyName} again to exit</Text>
|
||||
) : (
|
||||
<Byline>
|
||||
{onBack && <KeyboardShortcutHint shortcut="←" action="go back" />}
|
||||
<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />
|
||||
{shell.status === 'running' && onKillShell && (
|
||||
<KeyboardShortcutHint shortcut="x" action="stop" />
|
||||
)}
|
||||
</Byline>
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
$[12] = onBack;
|
||||
$[13] = onDone;
|
||||
$[14] = onKillShell;
|
||||
$[15] = shell.status;
|
||||
$[16] = t7;
|
||||
} else {
|
||||
t7 = $[16];
|
||||
}
|
||||
const handleKeyDown = t7;
|
||||
const isMonitor = shell.kind === "monitor";
|
||||
let t8;
|
||||
if ($[17] !== shell.command) {
|
||||
t8 = truncateToWidth(shell.command, 280);
|
||||
$[17] = shell.command;
|
||||
$[18] = t8;
|
||||
} else {
|
||||
t8 = $[18];
|
||||
}
|
||||
const displayCommand = t8;
|
||||
const t9 = isMonitor ? "Monitor details" : "Shell details";
|
||||
let t10;
|
||||
if ($[19] !== onBack || $[20] !== onKillShell || $[21] !== shell.status) {
|
||||
t10 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{shell.status === "running" && onKillShell && <KeyboardShortcutHint shortcut="x" action="stop" />}</Byline>;
|
||||
$[19] = onBack;
|
||||
$[20] = onKillShell;
|
||||
$[21] = shell.status;
|
||||
$[22] = t10;
|
||||
} else {
|
||||
t10 = $[22];
|
||||
}
|
||||
let t11;
|
||||
if ($[23] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t11 = <Text bold={true}>Status:</Text>;
|
||||
$[23] = t11;
|
||||
} else {
|
||||
t11 = $[23];
|
||||
}
|
||||
let t12;
|
||||
if ($[24] !== shell.result || $[25] !== shell.status) {
|
||||
t12 = <Text>{t11}{" "}{shell.status === "running" ? <Text color="background">{shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`}</Text> : shell.status === "completed" ? <Text color="success">{shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`}</Text> : <Text color="error">{shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`}</Text>}</Text>;
|
||||
$[24] = shell.result;
|
||||
$[25] = shell.status;
|
||||
$[26] = t12;
|
||||
} else {
|
||||
t12 = $[26];
|
||||
}
|
||||
let t13;
|
||||
if ($[27] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t13 = <Text bold={true}>Runtime:</Text>;
|
||||
$[27] = t13;
|
||||
} else {
|
||||
t13 = $[27];
|
||||
}
|
||||
let t14;
|
||||
if ($[28] !== shell.endTime) {
|
||||
t14 = shell.endTime ?? Date.now();
|
||||
$[28] = shell.endTime;
|
||||
$[29] = t14;
|
||||
} else {
|
||||
t14 = $[29];
|
||||
}
|
||||
const t15 = t14 - shell.startTime;
|
||||
let t16;
|
||||
if ($[30] !== t15) {
|
||||
t16 = formatDuration(t15);
|
||||
$[30] = t15;
|
||||
$[31] = t16;
|
||||
} else {
|
||||
t16 = $[31];
|
||||
}
|
||||
let t17;
|
||||
if ($[32] !== t16) {
|
||||
t17 = <Text>{t13}{" "}{t16}</Text>;
|
||||
$[32] = t16;
|
||||
$[33] = t17;
|
||||
} else {
|
||||
t17 = $[33];
|
||||
}
|
||||
const t18 = isMonitor ? "Script:" : "Command:";
|
||||
let t19;
|
||||
if ($[34] !== t18) {
|
||||
t19 = <Text bold={true}>{t18}</Text>;
|
||||
$[34] = t18;
|
||||
$[35] = t19;
|
||||
} else {
|
||||
t19 = $[35];
|
||||
}
|
||||
let t20;
|
||||
if ($[36] !== displayCommand || $[37] !== t19) {
|
||||
t20 = <Text wrap="wrap">{t19}{" "}{displayCommand}</Text>;
|
||||
$[36] = displayCommand;
|
||||
$[37] = t19;
|
||||
$[38] = t20;
|
||||
} else {
|
||||
t20 = $[38];
|
||||
}
|
||||
let t21;
|
||||
if ($[39] !== t12 || $[40] !== t17 || $[41] !== t20) {
|
||||
t21 = <Box flexDirection="column">{t12}{t17}{t20}</Box>;
|
||||
$[39] = t12;
|
||||
$[40] = t17;
|
||||
$[41] = t20;
|
||||
$[42] = t21;
|
||||
} else {
|
||||
t21 = $[42];
|
||||
}
|
||||
let t22;
|
||||
if ($[43] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t22 = <Text bold={true}>Output:</Text>;
|
||||
$[43] = t22;
|
||||
} else {
|
||||
t22 = $[43];
|
||||
}
|
||||
let t23;
|
||||
if ($[44] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t23 = <Text dimColor={true}>Loading output…</Text>;
|
||||
$[44] = t23;
|
||||
} else {
|
||||
t23 = $[44];
|
||||
}
|
||||
let t24;
|
||||
if ($[45] !== columns || $[46] !== deferredOutputPromise) {
|
||||
t24 = <Box flexDirection="column">{t22}<Suspense fallback={t23}><ShellOutputContent outputPromise={deferredOutputPromise} columns={columns} /></Suspense></Box>;
|
||||
$[45] = columns;
|
||||
$[46] = deferredOutputPromise;
|
||||
$[47] = t24;
|
||||
} else {
|
||||
t24 = $[47];
|
||||
}
|
||||
let t25;
|
||||
if ($[48] !== handleClose || $[49] !== t10 || $[50] !== t21 || $[51] !== t24 || $[52] !== t9) {
|
||||
t25 = <Dialog title={t9} onCancel={handleClose} color="background" inputGuide={t10}>{t21}{t24}</Dialog>;
|
||||
$[48] = handleClose;
|
||||
$[49] = t10;
|
||||
$[50] = t21;
|
||||
$[51] = t24;
|
||||
$[52] = t9;
|
||||
$[53] = t25;
|
||||
} else {
|
||||
t25 = $[53];
|
||||
}
|
||||
let t26;
|
||||
if ($[54] !== handleKeyDown || $[55] !== t25) {
|
||||
t26 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t25}</Box>;
|
||||
$[54] = handleKeyDown;
|
||||
$[55] = t25;
|
||||
$[56] = t26;
|
||||
} else {
|
||||
t26 = $[56];
|
||||
}
|
||||
return t26;
|
||||
}
|
||||
function _temp(setOutputPromise_0, shell_0) {
|
||||
return setOutputPromise_0(getTaskOutput(shell_0));
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
<Text bold>Status:</Text>{' '}
|
||||
{shell.status === 'running' ? (
|
||||
<Text color="background">
|
||||
{shell.status}
|
||||
{shell.result?.code !== undefined &&
|
||||
` (exit code: ${shell.result.code})`}
|
||||
</Text>
|
||||
) : shell.status === 'completed' ? (
|
||||
<Text color="success">
|
||||
{shell.status}
|
||||
{shell.result?.code !== undefined &&
|
||||
` (exit code: ${shell.result.code})`}
|
||||
</Text>
|
||||
) : (
|
||||
<Text color="error">
|
||||
{shell.status}
|
||||
{shell.result?.code !== undefined &&
|
||||
` (exit code: ${shell.result.code})`}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
<Text>
|
||||
<Text bold>Runtime:</Text>{' '}
|
||||
{formatDuration((shell.endTime ?? Date.now()) - shell.startTime)}
|
||||
</Text>
|
||||
<Text wrap="wrap">
|
||||
<Text bold>{isMonitor ? 'Script:' : 'Command:'}</Text>{' '}
|
||||
{displayCommand}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text bold>Output:</Text>
|
||||
<Suspense fallback={<Text dimColor>Loading output…</Text>}>
|
||||
<ShellOutputContent
|
||||
outputPromise={deferredOutputPromise}
|
||||
columns={columns}
|
||||
/>
|
||||
</Suspense>
|
||||
</Box>
|
||||
</Dialog>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
type ShellOutputContentProps = {
|
||||
outputPromise: Promise<TaskOutputResult>;
|
||||
columns: number;
|
||||
};
|
||||
function ShellOutputContent(t0) {
|
||||
const $ = _c(19);
|
||||
const {
|
||||
outputPromise,
|
||||
columns
|
||||
} = t0;
|
||||
const {
|
||||
content,
|
||||
bytesTotal
|
||||
} = use(outputPromise) as any;
|
||||
outputPromise: Promise<TaskOutputResult>
|
||||
columns: number
|
||||
}
|
||||
|
||||
function ShellOutputContent({
|
||||
outputPromise,
|
||||
columns,
|
||||
}: ShellOutputContentProps): React.ReactNode {
|
||||
const { content, bytesTotal } = use(outputPromise)
|
||||
|
||||
if (!content) {
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text dimColor={true}>No output available</Text>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
return t1;
|
||||
return <Text dimColor>No output available</Text>
|
||||
}
|
||||
let isIncomplete;
|
||||
let rendered;
|
||||
if ($[1] !== bytesTotal || $[2] !== content) {
|
||||
const starts = [];
|
||||
let pos = content.length;
|
||||
for (let i = 0; i < 10 && pos > 0; i++) {
|
||||
const prev = content.lastIndexOf("\n", pos - 1);
|
||||
starts.push(prev + 1);
|
||||
pos = prev;
|
||||
}
|
||||
starts.reverse();
|
||||
isIncomplete = bytesTotal > content.length;
|
||||
rendered = [];
|
||||
for (let i_0 = 0; i_0 < starts.length; i_0++) {
|
||||
const start = starts[i_0];
|
||||
const end = i_0 < starts.length - 1 ? starts[i_0 + 1] - 1 : content.length;
|
||||
const line = content.slice(start, end);
|
||||
if (line) {
|
||||
rendered.push(line);
|
||||
}
|
||||
}
|
||||
$[1] = bytesTotal;
|
||||
$[2] = content;
|
||||
$[3] = isIncomplete;
|
||||
$[4] = rendered;
|
||||
} else {
|
||||
isIncomplete = $[3];
|
||||
rendered = $[4];
|
||||
|
||||
// Find last 10 line boundaries via lastIndexOf
|
||||
const starts: number[] = []
|
||||
let pos = content.length
|
||||
for (let i = 0; i < 10 && pos > 0; i++) {
|
||||
const prev = content.lastIndexOf('\n', pos - 1)
|
||||
starts.push(prev + 1)
|
||||
pos = prev
|
||||
}
|
||||
const t1 = columns - 6;
|
||||
let t2;
|
||||
if ($[5] !== rendered) {
|
||||
t2 = rendered.map(_temp2);
|
||||
$[5] = rendered;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
starts.reverse()
|
||||
const isIncomplete = bytesTotal > content.length
|
||||
|
||||
// Build lines, skip empty trailing/leading segments
|
||||
const rendered: string[] = []
|
||||
for (let i = 0; i < starts.length; i++) {
|
||||
const start = starts[i]!
|
||||
const end = i < starts.length - 1 ? starts[i + 1]! - 1 : content.length
|
||||
const line = content.slice(start, end)
|
||||
if (line) rendered.push(line)
|
||||
}
|
||||
let t3;
|
||||
if ($[7] !== t1 || $[8] !== t2) {
|
||||
t3 = <Box borderStyle="round" paddingX={1} flexDirection="column" height={12} maxWidth={t1}>{t2}</Box>;
|
||||
$[7] = t1;
|
||||
$[8] = t2;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
}
|
||||
const t4 = `Showing ${rendered.length} lines`;
|
||||
let t5;
|
||||
if ($[10] !== bytesTotal || $[11] !== isIncomplete) {
|
||||
t5 = isIncomplete ? ` of ${formatFileSize(bytesTotal)}` : "";
|
||||
$[10] = bytesTotal;
|
||||
$[11] = isIncomplete;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
let t6;
|
||||
if ($[13] !== t4 || $[14] !== t5) {
|
||||
t6 = <Text dimColor={true} italic={true}>{t4}{t5}</Text>;
|
||||
$[13] = t4;
|
||||
$[14] = t5;
|
||||
$[15] = t6;
|
||||
} else {
|
||||
t6 = $[15];
|
||||
}
|
||||
let t7;
|
||||
if ($[16] !== t3 || $[17] !== t6) {
|
||||
t7 = <>{t3}{t6}</>;
|
||||
$[16] = t3;
|
||||
$[17] = t6;
|
||||
$[18] = t7;
|
||||
} else {
|
||||
t7 = $[18];
|
||||
}
|
||||
return t7;
|
||||
}
|
||||
function _temp2(line_0, i_1) {
|
||||
return <Text key={i_1} wrap="truncate-end">{line_0}</Text>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
paddingX={1}
|
||||
flexDirection="column"
|
||||
height={12}
|
||||
maxWidth={columns - 6}
|
||||
>
|
||||
{rendered.map((line, i) => (
|
||||
<Text key={i} wrap="truncate-end">
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
<Text dimColor italic>
|
||||
{`Showing ${rendered.length} lines`}
|
||||
{isIncomplete ? ` of ${formatFileSize(bytesTotal)}` : ''}
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,86 +1,52 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { Text } from 'src/ink.js';
|
||||
import type { TaskStatus } from 'src/Task.js';
|
||||
import type { LocalShellTaskState } from 'src/tasks/LocalShellTask/guards.js';
|
||||
import type { DeepImmutable } from 'src/types/utils.js';
|
||||
import type { ReactNode } from 'react'
|
||||
import React from 'react'
|
||||
import { Text } from 'src/ink.js'
|
||||
import type { TaskStatus } from 'src/Task.js'
|
||||
import type { LocalShellTaskState } from 'src/tasks/LocalShellTask/guards.js'
|
||||
import type { DeepImmutable } from 'src/types/utils.js'
|
||||
|
||||
type TaskStatusTextProps = {
|
||||
status: TaskStatus;
|
||||
label?: string;
|
||||
suffix?: string;
|
||||
};
|
||||
export function TaskStatusText(t0) {
|
||||
const $ = _c(4);
|
||||
const {
|
||||
status,
|
||||
label,
|
||||
suffix
|
||||
} = t0;
|
||||
const displayLabel = label ?? status;
|
||||
const color = status === "completed" ? "success" : status === "failed" ? "error" : status === "killed" ? "warning" : undefined;
|
||||
let t1;
|
||||
if ($[0] !== color || $[1] !== displayLabel || $[2] !== suffix) {
|
||||
t1 = <Text color={color} dimColor={true}>({displayLabel}{suffix})</Text>;
|
||||
$[0] = color;
|
||||
$[1] = displayLabel;
|
||||
$[2] = suffix;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
status: TaskStatus
|
||||
label?: string
|
||||
suffix?: string
|
||||
}
|
||||
export function ShellProgress(t0) {
|
||||
const $ = _c(4);
|
||||
const {
|
||||
shell
|
||||
} = t0;
|
||||
|
||||
export function TaskStatusText({
|
||||
status,
|
||||
label,
|
||||
suffix,
|
||||
}: TaskStatusTextProps): ReactNode {
|
||||
const displayLabel = label ?? status
|
||||
const color =
|
||||
status === 'completed'
|
||||
? 'success'
|
||||
: status === 'failed'
|
||||
? 'error'
|
||||
: status === 'killed'
|
||||
? 'warning'
|
||||
: undefined
|
||||
return (
|
||||
<Text color={color} dimColor>
|
||||
({displayLabel}
|
||||
{suffix})
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export function ShellProgress({
|
||||
shell,
|
||||
}: {
|
||||
shell: DeepImmutable<LocalShellTaskState>
|
||||
}): ReactNode {
|
||||
switch (shell.status) {
|
||||
case "completed":
|
||||
{
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <TaskStatusText status="completed" label="done" />;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case "failed":
|
||||
{
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <TaskStatusText status="failed" label="error" />;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case "killed":
|
||||
{
|
||||
let t1;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <TaskStatusText status="killed" label="stopped" />;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case "running":
|
||||
case "pending":
|
||||
{
|
||||
let t1;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <TaskStatusText status="running" />;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case 'completed':
|
||||
return <TaskStatusText status="completed" label="done" />
|
||||
case 'failed':
|
||||
return <TaskStatusText status="failed" label="error" />
|
||||
case 'killed':
|
||||
return <TaskStatusText status="killed" label="stopped" />
|
||||
case 'running':
|
||||
case 'pending':
|
||||
return <TaskStatusText status="running" />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
import React from 'react';
|
||||
import { Text } from '../../ink.js';
|
||||
import type { Tools } from '../../Tool.js';
|
||||
import { findToolByName } from '../../Tool.js';
|
||||
import type { ToolActivity } from '../../tasks/LocalAgentTask/LocalAgentTask.js';
|
||||
import type { ThemeName } from '../../utils/theme.js';
|
||||
export function renderToolActivity(activity: ToolActivity, tools: Tools, theme: ThemeName): React.ReactNode {
|
||||
const tool = findToolByName(tools, activity.toolName);
|
||||
import React from 'react'
|
||||
import { Text } from '../../ink.js'
|
||||
import type { Tools } from '../../Tool.js'
|
||||
import { findToolByName } from '../../Tool.js'
|
||||
import type { ToolActivity } from '../../tasks/LocalAgentTask/LocalAgentTask.js'
|
||||
import type { ThemeName } from '../../utils/theme.js'
|
||||
|
||||
export function renderToolActivity(
|
||||
activity: ToolActivity,
|
||||
tools: Tools,
|
||||
theme: ThemeName,
|
||||
): React.ReactNode {
|
||||
const tool = findToolByName(tools, activity.toolName)
|
||||
if (!tool) {
|
||||
return activity.toolName;
|
||||
return activity.toolName
|
||||
}
|
||||
try {
|
||||
const parsed = tool.inputSchema.safeParse(activity.input);
|
||||
const parsedInput = parsed.success ? parsed.data : {};
|
||||
const userFacingName = tool.userFacingName(parsedInput);
|
||||
const parsed = tool.inputSchema.safeParse(activity.input)
|
||||
const parsedInput = parsed.success ? parsed.data : {}
|
||||
const userFacingName = tool.userFacingName(parsedInput)
|
||||
if (!userFacingName) {
|
||||
return activity.toolName;
|
||||
return activity.toolName
|
||||
}
|
||||
const toolArgs = tool.renderToolUseMessage(parsedInput, {
|
||||
theme,
|
||||
verbose: false
|
||||
});
|
||||
verbose: false,
|
||||
})
|
||||
if (toolArgs) {
|
||||
return <Text>
|
||||
return (
|
||||
<Text>
|
||||
{userFacingName}({toolArgs})
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return userFacingName;
|
||||
return userFacingName
|
||||
} catch {
|
||||
return activity.toolName;
|
||||
return activity.toolName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,71 +2,73 @@
|
||||
* Shared utilities for displaying task status across different task types.
|
||||
*/
|
||||
|
||||
import figures from 'figures';
|
||||
import type { TaskStatus } from 'src/Task.js';
|
||||
import type { InProcessTeammateTaskState } from 'src/tasks/InProcessTeammateTask/types.js';
|
||||
import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js';
|
||||
import { isBackgroundTask, type TaskState } from 'src/tasks/types.js';
|
||||
import type { DeepImmutable } from 'src/types/utils.js';
|
||||
import { summarizeRecentActivities } from 'src/utils/collapseReadSearch.js';
|
||||
import figures from 'figures'
|
||||
import type { TaskStatus } from 'src/Task.js'
|
||||
import type { InProcessTeammateTaskState } from 'src/tasks/InProcessTeammateTask/types.js'
|
||||
import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
|
||||
import { isBackgroundTask, type TaskState } from 'src/tasks/types.js'
|
||||
import type { DeepImmutable } from 'src/types/utils.js'
|
||||
import { summarizeRecentActivities } from 'src/utils/collapseReadSearch.js'
|
||||
|
||||
/**
|
||||
* Returns true if the given task status represents a terminal (finished) state.
|
||||
*/
|
||||
export function isTerminalStatus(status: TaskStatus): boolean {
|
||||
return status === 'completed' || status === 'failed' || status === 'killed';
|
||||
return status === 'completed' || status === 'failed' || status === 'killed'
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the appropriate icon for a task based on status and state flags.
|
||||
*/
|
||||
export function getTaskStatusIcon(status: TaskStatus, options?: {
|
||||
isIdle?: boolean;
|
||||
awaitingApproval?: boolean;
|
||||
hasError?: boolean;
|
||||
shutdownRequested?: boolean;
|
||||
}): string {
|
||||
const {
|
||||
isIdle,
|
||||
awaitingApproval,
|
||||
hasError,
|
||||
shutdownRequested
|
||||
} = options ?? {};
|
||||
if (hasError) return figures.cross;
|
||||
if (awaitingApproval) return figures.questionMarkPrefix;
|
||||
if (shutdownRequested) return figures.warning;
|
||||
export function getTaskStatusIcon(
|
||||
status: TaskStatus,
|
||||
options?: {
|
||||
isIdle?: boolean
|
||||
awaitingApproval?: boolean
|
||||
hasError?: boolean
|
||||
shutdownRequested?: boolean
|
||||
},
|
||||
): string {
|
||||
const { isIdle, awaitingApproval, hasError, shutdownRequested } =
|
||||
options ?? {}
|
||||
|
||||
if (hasError) return figures.cross
|
||||
if (awaitingApproval) return figures.questionMarkPrefix
|
||||
if (shutdownRequested) return figures.warning
|
||||
|
||||
if (status === 'running') {
|
||||
if (isIdle) return figures.ellipsis;
|
||||
return figures.play;
|
||||
if (isIdle) return figures.ellipsis
|
||||
return figures.play
|
||||
}
|
||||
if (status === 'completed') return figures.tick;
|
||||
if (status === 'failed' || status === 'killed') return figures.cross;
|
||||
return figures.bullet;
|
||||
if (status === 'completed') return figures.tick
|
||||
if (status === 'failed' || status === 'killed') return figures.cross
|
||||
return figures.bullet
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the appropriate semantic color for a task based on status and state flags.
|
||||
*/
|
||||
export function getTaskStatusColor(status: TaskStatus, options?: {
|
||||
isIdle?: boolean;
|
||||
awaitingApproval?: boolean;
|
||||
hasError?: boolean;
|
||||
shutdownRequested?: boolean;
|
||||
}): 'success' | 'error' | 'warning' | 'background' {
|
||||
const {
|
||||
isIdle,
|
||||
awaitingApproval,
|
||||
hasError,
|
||||
shutdownRequested
|
||||
} = options ?? {};
|
||||
if (hasError) return 'error';
|
||||
if (awaitingApproval) return 'warning';
|
||||
if (shutdownRequested) return 'warning';
|
||||
if (isIdle) return 'background';
|
||||
if (status === 'completed') return 'success';
|
||||
if (status === 'failed') return 'error';
|
||||
if (status === 'killed') return 'warning';
|
||||
return 'background';
|
||||
export function getTaskStatusColor(
|
||||
status: TaskStatus,
|
||||
options?: {
|
||||
isIdle?: boolean
|
||||
awaitingApproval?: boolean
|
||||
hasError?: boolean
|
||||
shutdownRequested?: boolean
|
||||
},
|
||||
): 'success' | 'error' | 'warning' | 'background' {
|
||||
const { isIdle, awaitingApproval, hasError, shutdownRequested } =
|
||||
options ?? {}
|
||||
|
||||
if (hasError) return 'error'
|
||||
if (awaitingApproval) return 'warning'
|
||||
if (shutdownRequested) return 'warning'
|
||||
if (isIdle) return 'background'
|
||||
|
||||
if (status === 'completed') return 'success'
|
||||
if (status === 'failed') return 'error'
|
||||
if (status === 'killed') return 'warning'
|
||||
return 'background'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,11 +76,18 @@ export function getTaskStatusColor(status: TaskStatus, options?: {
|
||||
* accounting for shutdown/approval/idle states and falling back through
|
||||
* recent-activity summary → last activity description → 'working'.
|
||||
*/
|
||||
export function describeTeammateActivity(t: DeepImmutable<InProcessTeammateTaskState>): string {
|
||||
if (t.shutdownRequested) return 'stopping';
|
||||
if (t.awaitingPlanApproval) return 'awaiting approval';
|
||||
if (t.isIdle) return 'idle';
|
||||
return (t.progress?.recentActivities && summarizeRecentActivities(t.progress.recentActivities)) ?? t.progress?.lastActivity?.activityDescription ?? 'working';
|
||||
export function describeTeammateActivity(
|
||||
t: DeepImmutable<InProcessTeammateTaskState>,
|
||||
): string {
|
||||
if (t.shutdownRequested) return 'stopping'
|
||||
if (t.awaitingPlanApproval) return 'awaiting approval'
|
||||
if (t.isIdle) return 'idle'
|
||||
return (
|
||||
(t.progress?.recentActivities &&
|
||||
summarizeRecentActivities(t.progress.recentActivities)) ??
|
||||
t.progress?.lastActivity?.activityDescription ??
|
||||
'working'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,17 +99,21 @@ export function describeTeammateActivity(t: DeepImmutable<InProcessTeammateTaskS
|
||||
* plus exclusion of panel-managed agent tasks for ants (those are shown
|
||||
* by CoordinatorTaskPanel).
|
||||
*/
|
||||
export function shouldHideTasksFooter(tasks: {
|
||||
[taskId: string]: TaskState;
|
||||
}, showSpinnerTree: boolean): boolean {
|
||||
if (!showSpinnerTree) return false;
|
||||
let hasVisibleTask = false;
|
||||
export function shouldHideTasksFooter(
|
||||
tasks: { [taskId: string]: TaskState },
|
||||
showSpinnerTree: boolean,
|
||||
): boolean {
|
||||
if (!showSpinnerTree) return false
|
||||
let hasVisibleTask = false
|
||||
for (const t of Object.values(tasks) as TaskState[]) {
|
||||
if (!isBackgroundTask(t) || (process.env.USER_TYPE) === 'ant' && isPanelAgentTask(t)) {
|
||||
continue;
|
||||
if (
|
||||
!isBackgroundTask(t) ||
|
||||
(process.env.USER_TYPE === 'ant' && isPanelAgentTask(t))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
hasVisibleTask = true;
|
||||
if (t.type !== 'in_process_teammate') return false;
|
||||
hasVisibleTask = true
|
||||
if (t.type !== 'in_process_teammate') return false
|
||||
}
|
||||
return hasVisibleTask;
|
||||
return hasVisibleTask
|
||||
}
|
||||
|
||||
@@ -1,64 +1,48 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { type ReactNode } from 'react';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
import { Dialog } from '../design-system/Dialog.js';
|
||||
import { useWizard } from './useWizard.js';
|
||||
import { WizardNavigationFooter } from './WizardNavigationFooter.js';
|
||||
import React, { type ReactNode } from 'react'
|
||||
import type { Theme } from '../../utils/theme.js'
|
||||
import { Dialog } from '../design-system/Dialog.js'
|
||||
import { useWizard } from './useWizard.js'
|
||||
import { WizardNavigationFooter } from './WizardNavigationFooter.js'
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
color?: keyof Theme;
|
||||
children: ReactNode;
|
||||
subtitle?: string;
|
||||
footerText?: ReactNode;
|
||||
};
|
||||
export function WizardDialogLayout(t0) {
|
||||
const $ = _c(11);
|
||||
const {
|
||||
title: titleOverride,
|
||||
color: t1,
|
||||
children,
|
||||
subtitle,
|
||||
footerText
|
||||
} = t0;
|
||||
const color = t1 === undefined ? "suggestion" : t1;
|
||||
title?: string
|
||||
color?: keyof Theme
|
||||
children: ReactNode
|
||||
subtitle?: string
|
||||
footerText?: ReactNode
|
||||
}
|
||||
|
||||
export function WizardDialogLayout({
|
||||
title: titleOverride,
|
||||
color = 'suggestion',
|
||||
children,
|
||||
subtitle,
|
||||
footerText,
|
||||
}: Props): ReactNode {
|
||||
const {
|
||||
currentStepIndex,
|
||||
totalSteps,
|
||||
title: providerTitle,
|
||||
showStepCounter,
|
||||
goBack
|
||||
} = useWizard();
|
||||
const title = titleOverride || providerTitle || "Wizard";
|
||||
const stepSuffix = showStepCounter !== false ? ` (${currentStepIndex + 1}/${totalSteps})` : "";
|
||||
const t2 = `${title}${stepSuffix}`;
|
||||
let t3;
|
||||
if ($[0] !== children || $[1] !== color || $[2] !== goBack || $[3] !== subtitle || $[4] !== t2) {
|
||||
t3 = <Dialog title={t2} subtitle={subtitle} onCancel={goBack} color={color} hideInputGuide={true} isCancelActive={false}>{children}</Dialog>;
|
||||
$[0] = children;
|
||||
$[1] = color;
|
||||
$[2] = goBack;
|
||||
$[3] = subtitle;
|
||||
$[4] = t2;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== footerText) {
|
||||
t4 = <WizardNavigationFooter instructions={footerText} />;
|
||||
$[6] = footerText;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] !== t3 || $[9] !== t4) {
|
||||
t5 = <>{t3}{t4}</>;
|
||||
$[8] = t3;
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
return t5;
|
||||
goBack,
|
||||
} = useWizard()
|
||||
const title = titleOverride || providerTitle || 'Wizard'
|
||||
const stepSuffix =
|
||||
showStepCounter !== false ? ` (${currentStepIndex + 1}/${totalSteps})` : ''
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
title={`${title}${stepSuffix}`}
|
||||
subtitle={subtitle}
|
||||
onCancel={goBack}
|
||||
color={color}
|
||||
hideInputGuide
|
||||
isCancelActive={false}
|
||||
>
|
||||
{children}
|
||||
</Dialog>
|
||||
<WizardNavigationFooter instructions={footerText} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
||||
import React, { type ReactNode } from 'react'
|
||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||
import { Byline } from '../design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||
|
||||
type Props = {
|
||||
instructions?: ReactNode;
|
||||
};
|
||||
instructions?: ReactNode
|
||||
}
|
||||
|
||||
export function WizardNavigationFooter({
|
||||
instructions = <Byline>
|
||||
instructions = (
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="go back"
|
||||
/>
|
||||
</Byline>
|
||||
),
|
||||
}: Props): ReactNode {
|
||||
const exitState = useExitOnCtrlCDWithKeybindings();
|
||||
return <Box marginLeft={3} marginTop={1}>
|
||||
const exitState = useExitOnCtrlCDWithKeybindings()
|
||||
|
||||
return (
|
||||
<Box marginLeft={3} marginTop={1}>
|
||||
<Text dimColor>
|
||||
{exitState.pending ? `Press ${exitState.keyName} again to exit` : instructions}
|
||||
{exitState.pending
|
||||
? `Press ${exitState.keyName} again to exit`
|
||||
: instructions}
|
||||
</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,156 +1,96 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { createContext, type ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
||||
import type { WizardContextValue, WizardProviderProps } from './types.js';
|
||||
import React, {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
|
||||
import type { WizardContextValue, WizardProviderProps } from './types.js'
|
||||
|
||||
// Use any here for the context since it will be cast properly when used
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const WizardContext = createContext<WizardContextValue<any> | null>(null);
|
||||
export function WizardProvider(t0) {
|
||||
const $ = _c(38);
|
||||
const {
|
||||
steps,
|
||||
initialData: t1,
|
||||
onComplete,
|
||||
onCancel,
|
||||
children,
|
||||
title,
|
||||
showStepCounter: t2
|
||||
} = t0;
|
||||
let t3;
|
||||
if ($[0] !== t1) {
|
||||
t3 = t1 === undefined ? {} as T : t1;
|
||||
$[0] = t1;
|
||||
$[1] = t3;
|
||||
} else {
|
||||
t3 = $[1];
|
||||
}
|
||||
const initialData = t3;
|
||||
const showStepCounter = t2 === undefined ? true : t2;
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||
const [wizardData, setWizardData] = useState(initialData);
|
||||
const [isCompleted, setIsCompleted] = useState(false);
|
||||
let t4;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = [];
|
||||
$[2] = t4;
|
||||
} else {
|
||||
t4 = $[2];
|
||||
}
|
||||
const [navigationHistory, setNavigationHistory] = useState(t4);
|
||||
useExitOnCtrlCDWithKeybindings();
|
||||
let t5;
|
||||
let t6;
|
||||
if ($[3] !== isCompleted || $[4] !== onComplete || $[5] !== wizardData) {
|
||||
t5 = () => {
|
||||
if (isCompleted) {
|
||||
setNavigationHistory([]);
|
||||
onComplete(wizardData);
|
||||
}
|
||||
};
|
||||
t6 = [isCompleted, wizardData, onComplete];
|
||||
$[3] = isCompleted;
|
||||
$[4] = onComplete;
|
||||
$[5] = wizardData;
|
||||
$[6] = t5;
|
||||
$[7] = t6;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
t6 = $[7];
|
||||
}
|
||||
useEffect(t5, t6);
|
||||
let t7;
|
||||
if ($[8] !== currentStepIndex || $[9] !== navigationHistory || $[10] !== steps.length) {
|
||||
t7 = () => {
|
||||
if (currentStepIndex < steps.length - 1) {
|
||||
if (navigationHistory.length > 0) {
|
||||
setNavigationHistory(prev => [...prev, currentStepIndex]);
|
||||
}
|
||||
setCurrentStepIndex(_temp);
|
||||
} else {
|
||||
setIsCompleted(true);
|
||||
}
|
||||
};
|
||||
$[8] = currentStepIndex;
|
||||
$[9] = navigationHistory;
|
||||
$[10] = steps.length;
|
||||
$[11] = t7;
|
||||
} else {
|
||||
t7 = $[11];
|
||||
}
|
||||
const goNext = t7;
|
||||
let t8;
|
||||
if ($[12] !== currentStepIndex || $[13] !== navigationHistory || $[14] !== onCancel) {
|
||||
t8 = () => {
|
||||
export const WizardContext = createContext<WizardContextValue<any> | null>(null)
|
||||
|
||||
export function WizardProvider<T extends Record<string, unknown>>({
|
||||
steps,
|
||||
initialData = {} as T,
|
||||
onComplete,
|
||||
onCancel,
|
||||
children,
|
||||
title,
|
||||
showStepCounter = true,
|
||||
}: WizardProviderProps<T>): ReactNode {
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(0)
|
||||
const [wizardData, setWizardData] = useState<T>(initialData)
|
||||
const [isCompleted, setIsCompleted] = useState(false)
|
||||
const [navigationHistory, setNavigationHistory] = useState<number[]>([])
|
||||
|
||||
useExitOnCtrlCDWithKeybindings()
|
||||
|
||||
// Handle completion in useEffect to avoid updating parent during render
|
||||
useEffect(() => {
|
||||
if (isCompleted) {
|
||||
setNavigationHistory([])
|
||||
void onComplete(wizardData)
|
||||
}
|
||||
}, [isCompleted, wizardData, onComplete])
|
||||
|
||||
const goNext = useCallback(() => {
|
||||
if (currentStepIndex < steps.length - 1) {
|
||||
// If we have history (non-linear flow), add current step to it
|
||||
if (navigationHistory.length > 0) {
|
||||
const previousStep = navigationHistory[navigationHistory.length - 1];
|
||||
if (previousStep !== undefined) {
|
||||
setNavigationHistory(_temp2);
|
||||
setCurrentStepIndex(previousStep);
|
||||
}
|
||||
} else {
|
||||
if (currentStepIndex > 0) {
|
||||
setCurrentStepIndex(_temp3);
|
||||
} else {
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
}
|
||||
}
|
||||
setNavigationHistory(prev => [...prev, currentStepIndex])
|
||||
}
|
||||
};
|
||||
$[12] = currentStepIndex;
|
||||
$[13] = navigationHistory;
|
||||
$[14] = onCancel;
|
||||
$[15] = t8;
|
||||
} else {
|
||||
t8 = $[15];
|
||||
}
|
||||
const goBack = t8;
|
||||
let t9;
|
||||
if ($[16] !== currentStepIndex || $[17] !== steps.length) {
|
||||
t9 = index => {
|
||||
|
||||
setCurrentStepIndex(prev => prev + 1)
|
||||
} else {
|
||||
// Mark as completed, which will trigger useEffect
|
||||
setIsCompleted(true)
|
||||
}
|
||||
}, [currentStepIndex, steps.length, navigationHistory])
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
// Check if we have navigation history to use
|
||||
if (navigationHistory.length > 0) {
|
||||
const previousStep = navigationHistory[navigationHistory.length - 1]
|
||||
if (previousStep !== undefined) {
|
||||
setNavigationHistory(prev => prev.slice(0, -1))
|
||||
setCurrentStepIndex(previousStep)
|
||||
}
|
||||
} else if (currentStepIndex > 0) {
|
||||
// Fallback to simple decrement if no history
|
||||
setCurrentStepIndex(prev => prev - 1)
|
||||
} else if (onCancel) {
|
||||
onCancel()
|
||||
}
|
||||
}, [currentStepIndex, navigationHistory, onCancel])
|
||||
|
||||
const goToStep = useCallback(
|
||||
(index: number) => {
|
||||
if (index >= 0 && index < steps.length) {
|
||||
setNavigationHistory(prev_3 => [...prev_3, currentStepIndex]);
|
||||
setCurrentStepIndex(index);
|
||||
// Push current step to history before jumping
|
||||
setNavigationHistory(prev => [...prev, currentStepIndex])
|
||||
setCurrentStepIndex(index)
|
||||
}
|
||||
};
|
||||
$[16] = currentStepIndex;
|
||||
$[17] = steps.length;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
t9 = $[18];
|
||||
}
|
||||
const goToStep = t9;
|
||||
let t10;
|
||||
if ($[19] !== onCancel) {
|
||||
t10 = () => {
|
||||
setNavigationHistory([]);
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
$[19] = onCancel;
|
||||
$[20] = t10;
|
||||
} else {
|
||||
t10 = $[20];
|
||||
}
|
||||
const cancel = t10;
|
||||
let t11;
|
||||
if ($[21] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t11 = updates => {
|
||||
setWizardData(prev_4 => ({
|
||||
...prev_4,
|
||||
...updates
|
||||
}));
|
||||
};
|
||||
$[21] = t11;
|
||||
} else {
|
||||
t11 = $[21];
|
||||
}
|
||||
const updateWizardData = t11;
|
||||
let t12;
|
||||
if ($[22] !== cancel || $[23] !== currentStepIndex || $[24] !== goBack || $[25] !== goNext || $[26] !== goToStep || $[27] !== showStepCounter || $[28] !== steps.length || $[29] !== title || $[30] !== wizardData) {
|
||||
t12 = {
|
||||
},
|
||||
[currentStepIndex, steps.length],
|
||||
)
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
setNavigationHistory([])
|
||||
if (onCancel) {
|
||||
onCancel()
|
||||
}
|
||||
}, [onCancel])
|
||||
|
||||
const updateWizardData = useCallback((updates: Partial<T>) => {
|
||||
setWizardData(prev => ({ ...prev, ...updates }))
|
||||
}, [])
|
||||
|
||||
const contextValue = useMemo<WizardContextValue<T>>(
|
||||
() => ({
|
||||
currentStepIndex,
|
||||
totalSteps: steps.length,
|
||||
wizardData,
|
||||
@@ -161,52 +101,31 @@ export function WizardProvider(t0) {
|
||||
goToStep,
|
||||
cancel,
|
||||
title,
|
||||
showStepCounter
|
||||
};
|
||||
$[22] = cancel;
|
||||
$[23] = currentStepIndex;
|
||||
$[24] = goBack;
|
||||
$[25] = goNext;
|
||||
$[26] = goToStep;
|
||||
$[27] = showStepCounter;
|
||||
$[28] = steps.length;
|
||||
$[29] = title;
|
||||
$[30] = wizardData;
|
||||
$[31] = t12;
|
||||
} else {
|
||||
t12 = $[31];
|
||||
}
|
||||
const contextValue = t12;
|
||||
const CurrentStepComponent = steps[currentStepIndex];
|
||||
showStepCounter,
|
||||
}),
|
||||
[
|
||||
currentStepIndex,
|
||||
steps.length,
|
||||
wizardData,
|
||||
updateWizardData,
|
||||
goNext,
|
||||
goBack,
|
||||
goToStep,
|
||||
cancel,
|
||||
title,
|
||||
showStepCounter,
|
||||
],
|
||||
)
|
||||
|
||||
const CurrentStepComponent = steps[currentStepIndex]
|
||||
|
||||
if (!CurrentStepComponent || isCompleted) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t13;
|
||||
if ($[32] !== CurrentStepComponent || $[33] !== children) {
|
||||
t13 = children || <CurrentStepComponent />;
|
||||
$[32] = CurrentStepComponent;
|
||||
$[33] = children;
|
||||
$[34] = t13;
|
||||
} else {
|
||||
t13 = $[34];
|
||||
}
|
||||
let t14;
|
||||
if ($[35] !== contextValue || $[36] !== t13) {
|
||||
t14 = <WizardContext.Provider value={contextValue}>{t13}</WizardContext.Provider>;
|
||||
$[35] = contextValue;
|
||||
$[36] = t13;
|
||||
$[37] = t14;
|
||||
} else {
|
||||
t14 = $[37];
|
||||
}
|
||||
return t14;
|
||||
}
|
||||
function _temp3(prev_2) {
|
||||
return prev_2 - 1;
|
||||
}
|
||||
function _temp2(prev_1) {
|
||||
return prev_1.slice(0, -1);
|
||||
}
|
||||
function _temp(prev_0) {
|
||||
return prev_0 + 1;
|
||||
|
||||
return (
|
||||
<WizardContext.Provider value={contextValue}>
|
||||
{children || <CurrentStepComponent />}
|
||||
</WizardContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user