mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 16:25:51 +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 React from 'react';
|
import {
|
||||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
import { Box, Text } from '../../ink.js';
|
logEvent,
|
||||||
import { FeedbackSurveyView, isValidResponseInput } from './FeedbackSurveyView.js';
|
} from 'src/services/analytics/index.js'
|
||||||
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { TranscriptSharePrompt } from './TranscriptSharePrompt.js';
|
import {
|
||||||
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js';
|
FeedbackSurveyView,
|
||||||
import type { FeedbackSurveyResponse } from './utils.js';
|
isValidResponseInput,
|
||||||
|
} from './FeedbackSurveyView.js'
|
||||||
|
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
|
||||||
|
import { TranscriptSharePrompt } from './TranscriptSharePrompt.js'
|
||||||
|
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'
|
||||||
|
import type { FeedbackSurveyResponse } from './utils.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
|
state:
|
||||||
lastResponse: FeedbackSurveyResponse | null;
|
| 'closed'
|
||||||
handleSelect: (selected: FeedbackSurveyResponse) => void;
|
| 'open'
|
||||||
handleTranscriptSelect?: (selected: TranscriptShareResponse) => void;
|
| 'thanks'
|
||||||
inputValue: string;
|
| 'transcript_prompt'
|
||||||
setInputValue: (value: string) => void;
|
| 'submitting'
|
||||||
onRequestFeedback?: () => void;
|
| 'submitted'
|
||||||
message?: string;
|
lastResponse: FeedbackSurveyResponse | null
|
||||||
};
|
handleSelect: (selected: FeedbackSurveyResponse) => void
|
||||||
export function FeedbackSurvey(t0) {
|
handleTranscriptSelect?: (selected: TranscriptShareResponse) => void
|
||||||
const $ = _c(16);
|
inputValue: string
|
||||||
const {
|
setInputValue: (value: string) => void
|
||||||
state,
|
onRequestFeedback?: () => void
|
||||||
lastResponse,
|
message?: string
|
||||||
handleSelect,
|
}
|
||||||
handleTranscriptSelect,
|
|
||||||
inputValue,
|
export function FeedbackSurvey({
|
||||||
setInputValue,
|
state,
|
||||||
onRequestFeedback,
|
lastResponse,
|
||||||
message
|
handleSelect,
|
||||||
} = t0;
|
handleTranscriptSelect,
|
||||||
if (state === "closed") {
|
inputValue,
|
||||||
return null;
|
setInputValue,
|
||||||
|
onRequestFeedback,
|
||||||
|
message,
|
||||||
|
}: Props): React.ReactNode {
|
||||||
|
if (state === 'closed') {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
if (state === "thanks") {
|
|
||||||
let t1;
|
if (state === 'thanks') {
|
||||||
if ($[0] !== inputValue || $[1] !== lastResponse || $[2] !== onRequestFeedback || $[3] !== setInputValue) {
|
return (
|
||||||
t1 = <FeedbackSurveyThanks lastResponse={lastResponse} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={onRequestFeedback} />;
|
<FeedbackSurveyThanks
|
||||||
$[0] = inputValue;
|
lastResponse={lastResponse}
|
||||||
$[1] = lastResponse;
|
inputValue={inputValue}
|
||||||
$[2] = onRequestFeedback;
|
setInputValue={setInputValue}
|
||||||
$[3] = setInputValue;
|
onRequestFeedback={onRequestFeedback}
|
||||||
$[4] = t1;
|
/>
|
||||||
} else {
|
)
|
||||||
t1 = $[4];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
}
|
||||||
if (state === "submitted") {
|
|
||||||
let t1;
|
if (state === 'submitted') {
|
||||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
return (
|
||||||
t1 = <Box marginTop={1}><Text color="success">{"\u2713"} Thanks for sharing your transcript!</Text></Box>;
|
<Box marginTop={1}>
|
||||||
$[5] = t1;
|
<Text color="success">
|
||||||
} else {
|
{'\u2713'} Thanks for sharing your transcript!
|
||||||
t1 = $[5];
|
</Text>
|
||||||
}
|
</Box>
|
||||||
return t1;
|
)
|
||||||
}
|
}
|
||||||
if (state === "submitting") {
|
|
||||||
let t1;
|
if (state === 'submitting') {
|
||||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
return (
|
||||||
t1 = <Box marginTop={1}><Text dimColor={true}>Sharing transcript{"\u2026"}</Text></Box>;
|
<Box marginTop={1}>
|
||||||
$[6] = t1;
|
<Text dimColor>Sharing transcript{'\u2026'}</Text>
|
||||||
} else {
|
</Box>
|
||||||
t1 = $[6];
|
)
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
}
|
||||||
if (state === "transcript_prompt") {
|
|
||||||
|
if (state === 'transcript_prompt') {
|
||||||
if (!handleTranscriptSelect) {
|
if (!handleTranscriptSelect) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
if (inputValue && !["1", "2", "3"].includes(inputValue)) {
|
// Hide prompt if user is typing non-response characters
|
||||||
return null;
|
if (inputValue && !['1', '2', '3'].includes(inputValue)) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
let t1;
|
return (
|
||||||
if ($[7] !== handleTranscriptSelect || $[8] !== inputValue || $[9] !== setInputValue) {
|
<TranscriptSharePrompt
|
||||||
t1 = <TranscriptSharePrompt onSelect={handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} />;
|
onSelect={handleTranscriptSelect}
|
||||||
$[7] = handleTranscriptSelect;
|
inputValue={inputValue}
|
||||||
$[8] = inputValue;
|
setInputValue={setInputValue}
|
||||||
$[9] = setInputValue;
|
/>
|
||||||
$[10] = t1;
|
)
|
||||||
} else {
|
|
||||||
t1 = $[10];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// state === 'open'
|
||||||
|
// Hide the survey if the user is typing anything other than a survey response.
|
||||||
|
// This prevents the survey from showing up when the user is typing a message,
|
||||||
|
// which can result in accidental survey submissions (e.g. "s3cmd").
|
||||||
if (inputValue && !isValidResponseInput(inputValue)) {
|
if (inputValue && !isValidResponseInput(inputValue)) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
let t1;
|
|
||||||
if ($[11] !== handleSelect || $[12] !== inputValue || $[13] !== message || $[14] !== setInputValue) {
|
return (
|
||||||
t1 = <FeedbackSurveyView onSelect={handleSelect} inputValue={inputValue} setInputValue={setInputValue} message={message} />;
|
<FeedbackSurveyView
|
||||||
$[11] = handleSelect;
|
onSelect={handleSelect}
|
||||||
$[12] = inputValue;
|
inputValue={inputValue}
|
||||||
$[13] = message;
|
setInputValue={setInputValue}
|
||||||
$[14] = setInputValue;
|
message={message}
|
||||||
$[15] = t1;
|
/>
|
||||||
} else {
|
)
|
||||||
t1 = $[15];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThanksProps = {
|
type ThanksProps = {
|
||||||
lastResponse: FeedbackSurveyResponse | null;
|
lastResponse: FeedbackSurveyResponse | null
|
||||||
inputValue: string;
|
inputValue: string
|
||||||
setInputValue: (value: string) => void;
|
setInputValue: (value: string) => void
|
||||||
onRequestFeedback?: () => void;
|
onRequestFeedback?: () => void
|
||||||
};
|
}
|
||||||
const isFollowUpDigit = (char: string): char is '1' => char === '1';
|
|
||||||
function FeedbackSurveyThanks(t0) {
|
const isFollowUpDigit = (char: string): char is '1' => char === '1'
|
||||||
const $ = _c(12);
|
|
||||||
const {
|
function FeedbackSurveyThanks({
|
||||||
lastResponse,
|
lastResponse,
|
||||||
|
inputValue,
|
||||||
|
setInputValue,
|
||||||
|
onRequestFeedback,
|
||||||
|
}: ThanksProps): React.ReactNode {
|
||||||
|
const showFollowUp = onRequestFeedback && lastResponse === 'good'
|
||||||
|
|
||||||
|
// Listen for "1" keypress to launch /feedback
|
||||||
|
useDebouncedDigitInput({
|
||||||
inputValue,
|
inputValue,
|
||||||
setInputValue,
|
setInputValue,
|
||||||
onRequestFeedback
|
isValidDigit: isFollowUpDigit,
|
||||||
} = t0;
|
enabled: Boolean(showFollowUp),
|
||||||
const showFollowUp = onRequestFeedback && lastResponse === "good";
|
once: true,
|
||||||
const t1 = Boolean(showFollowUp);
|
onDigit: () => {
|
||||||
let t2;
|
logEvent('tengu_feedback_survey_event', {
|
||||||
if ($[0] !== lastResponse || $[1] !== onRequestFeedback) {
|
event_type:
|
||||||
t2 = () => {
|
'followup_accepted' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
logEvent("tengu_feedback_survey_event", {
|
response:
|
||||||
event_type: "followup_accepted" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
lastResponse as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
response: lastResponse as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
})
|
||||||
});
|
onRequestFeedback?.()
|
||||||
onRequestFeedback?.();
|
},
|
||||||
};
|
})
|
||||||
$[0] = lastResponse;
|
|
||||||
$[1] = onRequestFeedback;
|
const feedbackCommand =
|
||||||
$[2] = t2;
|
process.env.USER_TYPE === 'ant' ? '/issue' : '/feedback'
|
||||||
} else {
|
|
||||||
t2 = $[2];
|
return (
|
||||||
}
|
<Box marginTop={1} flexDirection="column">
|
||||||
let t3;
|
<Text color="success">Thanks for the feedback!</Text>
|
||||||
if ($[3] !== inputValue || $[4] !== setInputValue || $[5] !== t1 || $[6] !== t2) {
|
{showFollowUp ? (
|
||||||
t3 = {
|
<Text dimColor>
|
||||||
inputValue,
|
(Optional) Press [<Text color="ansi:cyan">1</Text>] to tell us what
|
||||||
setInputValue,
|
went well {' \u00b7 '}
|
||||||
isValidDigit: isFollowUpDigit,
|
{feedbackCommand}
|
||||||
enabled: t1,
|
</Text>
|
||||||
once: true,
|
) : lastResponse === 'bad' ? (
|
||||||
onDigit: t2
|
<Text dimColor>Use /issue to report model behavior issues.</Text>
|
||||||
};
|
) : (
|
||||||
$[3] = inputValue;
|
<Text dimColor>
|
||||||
$[4] = setInputValue;
|
Use {feedbackCommand} to share detailed feedback anytime.
|
||||||
$[5] = t1;
|
</Text>
|
||||||
$[6] = t2;
|
)}
|
||||||
$[7] = t3;
|
</Box>
|
||||||
} else {
|
)
|
||||||
t3 = $[7];
|
|
||||||
}
|
|
||||||
useDebouncedDigitInput(t3);
|
|
||||||
const feedbackCommand = false ? "/issue" : "/feedback";
|
|
||||||
let t4;
|
|
||||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t4 = <Text color="success">Thanks for the feedback!</Text>;
|
|
||||||
$[8] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[8];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[9] !== lastResponse || $[10] !== showFollowUp) {
|
|
||||||
t5 = <Box marginTop={1} flexDirection="column">{t4}{showFollowUp ? <Text dimColor={true}>(Optional) Press [<Text color="ansi:cyan">1</Text>] to tell us what went well {" \xB7 "}{feedbackCommand}</Text> : lastResponse === "bad" ? <Text dimColor={true}>Use /issue to report model behavior issues.</Text> : <Text dimColor={true}>Use {feedbackCommand} to share detailed feedback anytime.</Text>}</Box>;
|
|
||||||
$[9] = lastResponse;
|
|
||||||
$[10] = showFollowUp;
|
|
||||||
$[11] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[11];
|
|
||||||
}
|
|
||||||
return t5;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,107 +1,72 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React from 'react'
|
||||||
import React from 'react';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'
|
||||||
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js';
|
import type { FeedbackSurveyResponse } from './utils.js'
|
||||||
import type { FeedbackSurveyResponse } from './utils.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSelect: (option: FeedbackSurveyResponse) => void;
|
onSelect: (option: FeedbackSurveyResponse) => void
|
||||||
inputValue: string;
|
inputValue: string
|
||||||
setInputValue: (value: string) => void;
|
setInputValue: (value: string) => void
|
||||||
message?: string;
|
message?: string
|
||||||
};
|
}
|
||||||
const RESPONSE_INPUTS = ['0', '1', '2', '3'] as const;
|
|
||||||
type ResponseInput = (typeof RESPONSE_INPUTS)[number];
|
const RESPONSE_INPUTS = ['0', '1', '2', '3'] as const
|
||||||
|
type ResponseInput = (typeof RESPONSE_INPUTS)[number]
|
||||||
|
|
||||||
const inputToResponse: Record<ResponseInput, FeedbackSurveyResponse> = {
|
const inputToResponse: Record<ResponseInput, FeedbackSurveyResponse> = {
|
||||||
'0': 'dismissed',
|
'0': 'dismissed',
|
||||||
'1': 'bad',
|
'1': 'bad',
|
||||||
'2': 'fine',
|
'2': 'fine',
|
||||||
'3': 'good'
|
'3': 'good',
|
||||||
} as const;
|
} as const
|
||||||
export const isValidResponseInput = (input: string): input is ResponseInput => (RESPONSE_INPUTS as readonly string[]).includes(input);
|
|
||||||
const DEFAULT_MESSAGE = 'How is Claude doing this session? (optional)';
|
export const isValidResponseInput = (input: string): input is ResponseInput =>
|
||||||
export function FeedbackSurveyView(t0) {
|
(RESPONSE_INPUTS as readonly string[]).includes(input)
|
||||||
const $ = _c(15);
|
|
||||||
const {
|
const DEFAULT_MESSAGE = 'How is Claude doing this session? (optional)'
|
||||||
onSelect,
|
|
||||||
|
export function FeedbackSurveyView({
|
||||||
|
onSelect,
|
||||||
|
inputValue,
|
||||||
|
setInputValue,
|
||||||
|
message = DEFAULT_MESSAGE,
|
||||||
|
}: Props): React.ReactNode {
|
||||||
|
useDebouncedDigitInput({
|
||||||
inputValue,
|
inputValue,
|
||||||
setInputValue,
|
setInputValue,
|
||||||
message: t1
|
isValidDigit: isValidResponseInput,
|
||||||
} = t0;
|
onDigit: digit => onSelect(inputToResponse[digit]),
|
||||||
const message = t1 === undefined ? DEFAULT_MESSAGE : t1;
|
})
|
||||||
let t2;
|
|
||||||
if ($[0] !== onSelect) {
|
return (
|
||||||
t2 = digit => onSelect(inputToResponse[digit]);
|
<Box flexDirection="column" marginTop={1}>
|
||||||
$[0] = onSelect;
|
<Box>
|
||||||
$[1] = t2;
|
<Text color="ansi:cyan">● </Text>
|
||||||
} else {
|
<Text bold>{message}</Text>
|
||||||
t2 = $[1];
|
</Box>
|
||||||
}
|
|
||||||
let t3;
|
<Box marginLeft={2}>
|
||||||
if ($[2] !== inputValue || $[3] !== setInputValue || $[4] !== t2) {
|
<Box width={10}>
|
||||||
t3 = {
|
<Text>
|
||||||
inputValue,
|
<Text color="ansi:cyan">1</Text>: Bad
|
||||||
setInputValue,
|
</Text>
|
||||||
isValidDigit: isValidResponseInput,
|
</Box>
|
||||||
onDigit: t2
|
<Box width={10}>
|
||||||
};
|
<Text>
|
||||||
$[2] = inputValue;
|
<Text color="ansi:cyan">2</Text>: Fine
|
||||||
$[3] = setInputValue;
|
</Text>
|
||||||
$[4] = t2;
|
</Box>
|
||||||
$[5] = t3;
|
<Box width={10}>
|
||||||
} else {
|
<Text>
|
||||||
t3 = $[5];
|
<Text color="ansi:cyan">3</Text>: Good
|
||||||
}
|
</Text>
|
||||||
useDebouncedDigitInput(t3);
|
</Box>
|
||||||
let t4;
|
<Box>
|
||||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
<Text>
|
||||||
t4 = <Text color="ansi:cyan">● </Text>;
|
<Text color="ansi:cyan">0</Text>: Dismiss
|
||||||
$[6] = t4;
|
</Text>
|
||||||
} else {
|
</Box>
|
||||||
t4 = $[6];
|
</Box>
|
||||||
}
|
</Box>
|
||||||
let t5;
|
)
|
||||||
if ($[7] !== message) {
|
|
||||||
t5 = <Box>{t4}<Text bold={true}>{message}</Text></Box>;
|
|
||||||
$[7] = message;
|
|
||||||
$[8] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[8];
|
|
||||||
}
|
|
||||||
let t6;
|
|
||||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t6 = <Box width={10}><Text><Text color="ansi:cyan">1</Text>: Bad</Text></Box>;
|
|
||||||
$[9] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[9];
|
|
||||||
}
|
|
||||||
let t7;
|
|
||||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t7 = <Box width={10}><Text><Text color="ansi:cyan">2</Text>: Fine</Text></Box>;
|
|
||||||
$[10] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[10];
|
|
||||||
}
|
|
||||||
let t8;
|
|
||||||
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t8 = <Box width={10}><Text><Text color="ansi:cyan">3</Text>: Good</Text></Box>;
|
|
||||||
$[11] = t8;
|
|
||||||
} else {
|
|
||||||
t8 = $[11];
|
|
||||||
}
|
|
||||||
let t9;
|
|
||||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t9 = <Box marginLeft={2}>{t6}{t7}{t8}<Box><Text><Text color="ansi:cyan">0</Text>: Dismiss</Text></Box></Box>;
|
|
||||||
$[12] = t9;
|
|
||||||
} else {
|
|
||||||
t9 = $[12];
|
|
||||||
}
|
|
||||||
let t10;
|
|
||||||
if ($[13] !== t5) {
|
|
||||||
t10 = <Box flexDirection="column" marginTop={1}>{t5}{t9}</Box>;
|
|
||||||
$[13] = t5;
|
|
||||||
$[14] = t10;
|
|
||||||
} else {
|
|
||||||
t10 = $[14];
|
|
||||||
}
|
|
||||||
return t10;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +1,74 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React from 'react'
|
||||||
import React from 'react';
|
import { BLACK_CIRCLE } from '../../constants/figures.js'
|
||||||
import { BLACK_CIRCLE } from '../../constants/figures.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'
|
||||||
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js';
|
|
||||||
export type TranscriptShareResponse = 'yes' | 'no' | 'dont_ask_again';
|
export type TranscriptShareResponse = 'yes' | 'no' | 'dont_ask_again'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSelect: (option: TranscriptShareResponse) => void;
|
onSelect: (option: TranscriptShareResponse) => void
|
||||||
inputValue: string;
|
inputValue: string
|
||||||
setInputValue: (value: string) => void;
|
setInputValue: (value: string) => void
|
||||||
};
|
}
|
||||||
const RESPONSE_INPUTS = ['1', '2', '3'] as const;
|
|
||||||
type ResponseInput = (typeof RESPONSE_INPUTS)[number];
|
const RESPONSE_INPUTS = ['1', '2', '3'] as const
|
||||||
|
type ResponseInput = (typeof RESPONSE_INPUTS)[number]
|
||||||
|
|
||||||
const inputToResponse: Record<ResponseInput, TranscriptShareResponse> = {
|
const inputToResponse: Record<ResponseInput, TranscriptShareResponse> = {
|
||||||
'1': 'yes',
|
'1': 'yes',
|
||||||
'2': 'no',
|
'2': 'no',
|
||||||
'3': 'dont_ask_again'
|
'3': 'dont_ask_again',
|
||||||
} as const;
|
} as const
|
||||||
const isValidResponseInput = (input: string): input is ResponseInput => (RESPONSE_INPUTS as readonly string[]).includes(input);
|
|
||||||
export function TranscriptSharePrompt(t0) {
|
const isValidResponseInput = (input: string): input is ResponseInput =>
|
||||||
const $ = _c(11);
|
(RESPONSE_INPUTS as readonly string[]).includes(input)
|
||||||
const {
|
|
||||||
onSelect,
|
export function TranscriptSharePrompt({
|
||||||
|
onSelect,
|
||||||
|
inputValue,
|
||||||
|
setInputValue,
|
||||||
|
}: Props): React.ReactNode {
|
||||||
|
useDebouncedDigitInput({
|
||||||
inputValue,
|
inputValue,
|
||||||
setInputValue
|
setInputValue,
|
||||||
} = t0;
|
isValidDigit: isValidResponseInput,
|
||||||
let t1;
|
onDigit: digit => onSelect(inputToResponse[digit]),
|
||||||
if ($[0] !== onSelect) {
|
})
|
||||||
t1 = digit => onSelect(inputToResponse[digit]);
|
|
||||||
$[0] = onSelect;
|
return (
|
||||||
$[1] = t1;
|
<Box flexDirection="column" marginTop={1}>
|
||||||
} else {
|
<Box>
|
||||||
t1 = $[1];
|
<Text color="ansi:cyan">{BLACK_CIRCLE} </Text>
|
||||||
}
|
<Text bold>
|
||||||
let t2;
|
Can Anthropic look at your session transcript to help us improve
|
||||||
if ($[2] !== inputValue || $[3] !== setInputValue || $[4] !== t1) {
|
Claude Code?
|
||||||
t2 = {
|
</Text>
|
||||||
inputValue,
|
</Box>
|
||||||
setInputValue,
|
|
||||||
isValidDigit: isValidResponseInput,
|
<Box marginLeft={2}>
|
||||||
onDigit: t1
|
<Text dimColor>
|
||||||
};
|
Learn more:
|
||||||
$[2] = inputValue;
|
https://code.claude.com/docs/en/data-usage#session-quality-surveys
|
||||||
$[3] = setInputValue;
|
</Text>
|
||||||
$[4] = t1;
|
</Box>
|
||||||
$[5] = t2;
|
|
||||||
} else {
|
<Box marginLeft={2}>
|
||||||
t2 = $[5];
|
<Box width={10}>
|
||||||
}
|
<Text>
|
||||||
useDebouncedDigitInput(t2);
|
<Text color="ansi:cyan">1</Text>: Yes
|
||||||
let t3;
|
</Text>
|
||||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
</Box>
|
||||||
t3 = <Box><Text color="ansi:cyan">{BLACK_CIRCLE} </Text><Text bold={true}>Can Anthropic look at your session transcript to help us improve Claude Code?</Text></Box>;
|
<Box width={10}>
|
||||||
$[6] = t3;
|
<Text>
|
||||||
} else {
|
<Text color="ansi:cyan">2</Text>: No
|
||||||
t3 = $[6];
|
</Text>
|
||||||
}
|
</Box>
|
||||||
let t4;
|
<Box>
|
||||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
<Text>
|
||||||
t4 = <Box marginLeft={2}><Text dimColor={true}>Learn more: https://code.claude.com/docs/en/data-usage#session-quality-surveys</Text></Box>;
|
<Text color="ansi:cyan">3</Text>: Don't ask again
|
||||||
$[7] = t4;
|
</Text>
|
||||||
} else {
|
</Box>
|
||||||
t4 = $[7];
|
</Box>
|
||||||
}
|
</Box>
|
||||||
let t5;
|
)
|
||||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t5 = <Box width={10}><Text><Text color="ansi:cyan">1</Text>: Yes</Text></Box>;
|
|
||||||
$[8] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[8];
|
|
||||||
}
|
|
||||||
let t6;
|
|
||||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t6 = <Box width={10}><Text><Text color="ansi:cyan">2</Text>: No</Text></Box>;
|
|
||||||
$[9] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[9];
|
|
||||||
}
|
|
||||||
let t7;
|
|
||||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t7 = <Box flexDirection="column" marginTop={1}>{t3}{t4}<Box marginLeft={2}>{t5}{t6}<Box><Text><Text color="ansi:cyan">3</Text>: Don't ask again</Text></Box></Box></Box>;
|
|
||||||
$[10] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[10];
|
|
||||||
}
|
|
||||||
return t7;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,41 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useDynamicConfig } from 'src/hooks/useDynamicConfig.js';
|
import { useDynamicConfig } from 'src/hooks/useDynamicConfig.js'
|
||||||
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js';
|
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'
|
||||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
import {
|
||||||
import { isPolicyAllowed } from '../../services/policyLimits/index.js';
|
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
import type { Message } from '../../types/message.js';
|
logEvent,
|
||||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
} from 'src/services/analytics/index.js'
|
||||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
import { isPolicyAllowed } from '../../services/policyLimits/index.js'
|
||||||
import { getLastAssistantMessage } from '../../utils/messages.js';
|
import type { Message } from '../../types/message.js'
|
||||||
import { getMainLoopModel } from '../../utils/model/model.js';
|
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||||
import { getInitialSettings } from '../../utils/settings/settings.js';
|
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||||
import { logOTelEvent } from '../../utils/telemetry/events.js';
|
import { getLastAssistantMessage } from '../../utils/messages.js'
|
||||||
import { submitTranscriptShare, type TranscriptShareTrigger } from './submitTranscriptShare.js';
|
import { getMainLoopModel } from '../../utils/model/model.js'
|
||||||
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
|
import { getInitialSettings } from '../../utils/settings/settings.js'
|
||||||
import { useSurveyState } from './useSurveyState.js';
|
import { logOTelEvent } from '../../utils/telemetry/events.js'
|
||||||
import type { FeedbackSurveyResponse, FeedbackSurveyType } from './utils.js';
|
import {
|
||||||
|
submitTranscriptShare,
|
||||||
|
type TranscriptShareTrigger,
|
||||||
|
} from './submitTranscriptShare.js'
|
||||||
|
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
|
||||||
|
import { useSurveyState } from './useSurveyState.js'
|
||||||
|
import type { FeedbackSurveyResponse, FeedbackSurveyType } from './utils.js'
|
||||||
|
|
||||||
type FeedbackSurveyConfig = {
|
type FeedbackSurveyConfig = {
|
||||||
minTimeBeforeFeedbackMs: number;
|
minTimeBeforeFeedbackMs: number
|
||||||
minTimeBetweenFeedbackMs: number;
|
minTimeBetweenFeedbackMs: number
|
||||||
minTimeBetweenGlobalFeedbackMs: number;
|
minTimeBetweenGlobalFeedbackMs: number
|
||||||
minUserTurnsBeforeFeedback: number;
|
minUserTurnsBeforeFeedback: number
|
||||||
minUserTurnsBetweenFeedback: number;
|
minUserTurnsBetweenFeedback: number
|
||||||
hideThanksAfterMs: number;
|
hideThanksAfterMs: number
|
||||||
onForModels: string[];
|
onForModels: string[]
|
||||||
probability: number;
|
probability: number
|
||||||
};
|
}
|
||||||
|
|
||||||
type TranscriptAskConfig = {
|
type TranscriptAskConfig = {
|
||||||
probability: number;
|
probability: number
|
||||||
};
|
}
|
||||||
|
|
||||||
const DEFAULT_FEEDBACK_SURVEY_CONFIG: FeedbackSurveyConfig = {
|
const DEFAULT_FEEDBACK_SURVEY_CONFIG: FeedbackSurveyConfig = {
|
||||||
minTimeBeforeFeedbackMs: 600000,
|
minTimeBeforeFeedbackMs: 600000,
|
||||||
minTimeBetweenFeedbackMs: 3600000,
|
minTimeBetweenFeedbackMs: 3600000,
|
||||||
@@ -35,261 +44,381 @@ const DEFAULT_FEEDBACK_SURVEY_CONFIG: FeedbackSurveyConfig = {
|
|||||||
minUserTurnsBetweenFeedback: 10,
|
minUserTurnsBetweenFeedback: 10,
|
||||||
hideThanksAfterMs: 3000,
|
hideThanksAfterMs: 3000,
|
||||||
onForModels: ['*'],
|
onForModels: ['*'],
|
||||||
probability: 0.005
|
probability: 0.005,
|
||||||
};
|
}
|
||||||
|
|
||||||
const DEFAULT_TRANSCRIPT_ASK_CONFIG: TranscriptAskConfig = {
|
const DEFAULT_TRANSCRIPT_ASK_CONFIG: TranscriptAskConfig = {
|
||||||
probability: 0
|
probability: 0,
|
||||||
};
|
}
|
||||||
export function useFeedbackSurvey(messages: Message[], isLoading: boolean, submitCount: number, surveyType: FeedbackSurveyType = 'session', hasActivePrompt: boolean = false): {
|
|
||||||
state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
|
export function useFeedbackSurvey(
|
||||||
lastResponse: FeedbackSurveyResponse | null;
|
messages: Message[],
|
||||||
handleSelect: (selected: FeedbackSurveyResponse) => boolean;
|
isLoading: boolean,
|
||||||
handleTranscriptSelect: (selected: TranscriptShareResponse) => void;
|
submitCount: number,
|
||||||
|
surveyType: FeedbackSurveyType = 'session',
|
||||||
|
hasActivePrompt: boolean = false,
|
||||||
|
): {
|
||||||
|
state:
|
||||||
|
| 'closed'
|
||||||
|
| 'open'
|
||||||
|
| 'thanks'
|
||||||
|
| 'transcript_prompt'
|
||||||
|
| 'submitting'
|
||||||
|
| 'submitted'
|
||||||
|
lastResponse: FeedbackSurveyResponse | null
|
||||||
|
handleSelect: (selected: FeedbackSurveyResponse) => boolean
|
||||||
|
handleTranscriptSelect: (selected: TranscriptShareResponse) => void
|
||||||
} {
|
} {
|
||||||
const lastAssistantMessageIdRef = useRef('unknown');
|
const lastAssistantMessageIdRef = useRef('unknown')
|
||||||
lastAssistantMessageIdRef.current = getLastAssistantMessage(messages)?.message?.id || 'unknown';
|
lastAssistantMessageIdRef.current =
|
||||||
|
getLastAssistantMessage(messages)?.message?.id || 'unknown'
|
||||||
const [feedbackSurvey, setFeedbackSurvey] = useState<{
|
const [feedbackSurvey, setFeedbackSurvey] = useState<{
|
||||||
timeLastShown: number | null;
|
timeLastShown: number | null
|
||||||
submitCountAtLastAppearance: number | null;
|
submitCountAtLastAppearance: number | null
|
||||||
}>(() => ({
|
}>(() => ({ timeLastShown: null, submitCountAtLastAppearance: null }))
|
||||||
timeLastShown: null,
|
const config = useDynamicConfig<FeedbackSurveyConfig>(
|
||||||
submitCountAtLastAppearance: null
|
'tengu_feedback_survey_config',
|
||||||
}));
|
DEFAULT_FEEDBACK_SURVEY_CONFIG,
|
||||||
const config = useDynamicConfig<FeedbackSurveyConfig>('tengu_feedback_survey_config', DEFAULT_FEEDBACK_SURVEY_CONFIG);
|
)
|
||||||
const badTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>('tengu_bad_survey_transcript_ask_config', DEFAULT_TRANSCRIPT_ASK_CONFIG);
|
const badTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>(
|
||||||
const goodTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>('tengu_good_survey_transcript_ask_config', DEFAULT_TRANSCRIPT_ASK_CONFIG);
|
'tengu_bad_survey_transcript_ask_config',
|
||||||
const settingsRate = getInitialSettings().feedbackSurveyRate;
|
DEFAULT_TRANSCRIPT_ASK_CONFIG,
|
||||||
const sessionStartTime = useRef(Date.now());
|
)
|
||||||
const submitCountAtSessionStart = useRef(submitCount);
|
const goodTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>(
|
||||||
const submitCountRef = useRef(submitCount);
|
'tengu_good_survey_transcript_ask_config',
|
||||||
submitCountRef.current = submitCount;
|
DEFAULT_TRANSCRIPT_ASK_CONFIG,
|
||||||
const messagesRef = useRef(messages);
|
)
|
||||||
messagesRef.current = messages;
|
const settingsRate = getInitialSettings().feedbackSurveyRate
|
||||||
|
const sessionStartTime = useRef(Date.now())
|
||||||
|
const submitCountAtSessionStart = useRef(submitCount)
|
||||||
|
const submitCountRef = useRef(submitCount)
|
||||||
|
submitCountRef.current = submitCount
|
||||||
|
const messagesRef = useRef(messages)
|
||||||
|
messagesRef.current = messages
|
||||||
// Probability gate: roll once when eligibility conditions are met, not on every
|
// Probability gate: roll once when eligibility conditions are met, not on every
|
||||||
// useMemo re-evaluation. Without this, each dependency change (submitCount,
|
// useMemo re-evaluation. Without this, each dependency change (submitCount,
|
||||||
// isLoading toggle, etc.) re-rolls Math.random(), making the survey almost
|
// isLoading toggle, etc.) re-rolls Math.random(), making the survey almost
|
||||||
// certain to appear after enough renders.
|
// certain to appear after enough renders.
|
||||||
const probabilityPassedRef = useRef(false);
|
const probabilityPassedRef = useRef(false)
|
||||||
const lastEligibleSubmitCountRef = useRef<number | null>(null);
|
const lastEligibleSubmitCountRef = useRef<number | null>(null)
|
||||||
const updateLastShownTime = useCallback((timestamp: number, submitCountValue: number) => {
|
|
||||||
setFeedbackSurvey(prev => {
|
const updateLastShownTime = useCallback(
|
||||||
if (prev.timeLastShown === timestamp && prev.submitCountAtLastAppearance === submitCountValue) {
|
(timestamp: number, submitCountValue: number) => {
|
||||||
return prev;
|
setFeedbackSurvey(prev => {
|
||||||
}
|
if (
|
||||||
return {
|
prev.timeLastShown === timestamp &&
|
||||||
timeLastShown: timestamp,
|
prev.submitCountAtLastAppearance === submitCountValue
|
||||||
submitCountAtLastAppearance: submitCountValue
|
) {
|
||||||
};
|
return prev
|
||||||
});
|
|
||||||
// Persist cross-session pacing state (previously done by onChangeAppState observer)
|
|
||||||
if (getGlobalConfig().feedbackSurveyState?.lastShownTime !== timestamp) {
|
|
||||||
saveGlobalConfig(current => ({
|
|
||||||
...current,
|
|
||||||
feedbackSurveyState: {
|
|
||||||
lastShownTime: timestamp
|
|
||||||
}
|
}
|
||||||
}));
|
return {
|
||||||
}
|
timeLastShown: timestamp,
|
||||||
}, []);
|
submitCountAtLastAppearance: submitCountValue,
|
||||||
const onOpen = useCallback((appearanceId: string) => {
|
}
|
||||||
updateLastShownTime(Date.now(), submitCountRef.current);
|
})
|
||||||
logEvent('tengu_feedback_survey_event', {
|
// Persist cross-session pacing state (previously done by onChangeAppState observer)
|
||||||
event_type: 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
if (getGlobalConfig().feedbackSurveyState?.lastShownTime !== timestamp) {
|
||||||
appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
saveGlobalConfig(current => ({
|
||||||
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
...current,
|
||||||
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
feedbackSurveyState: {
|
||||||
});
|
lastShownTime: timestamp,
|
||||||
void logOTelEvent('feedback_survey', {
|
},
|
||||||
event_type: 'appeared',
|
}))
|
||||||
appearance_id: appearanceId,
|
}
|
||||||
survey_type: surveyType
|
},
|
||||||
});
|
[],
|
||||||
}, [updateLastShownTime, surveyType]);
|
)
|
||||||
const onSelect = useCallback((appearanceId_0: string, selected: FeedbackSurveyResponse) => {
|
|
||||||
updateLastShownTime(Date.now(), submitCountRef.current);
|
|
||||||
logEvent('tengu_feedback_survey_event', {
|
|
||||||
event_type: 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
|
||||||
});
|
|
||||||
void logOTelEvent('feedback_survey', {
|
|
||||||
event_type: 'responded',
|
|
||||||
appearance_id: appearanceId_0,
|
|
||||||
response: selected,
|
|
||||||
survey_type: surveyType
|
|
||||||
});
|
|
||||||
}, [updateLastShownTime, surveyType]);
|
|
||||||
const shouldShowTranscriptPrompt = useCallback((selected_0: FeedbackSurveyResponse) => {
|
|
||||||
// Only bad and good ratings trigger the transcript ask
|
|
||||||
if (selected_0 !== 'bad' && selected_0 !== 'good') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't show if user previously chose "Don't ask again"
|
const onOpen = useCallback(
|
||||||
if (getGlobalConfig().transcriptShareDismissed) {
|
(appearanceId: string) => {
|
||||||
return false;
|
updateLastShownTime(Date.now(), submitCountRef.current)
|
||||||
}
|
|
||||||
|
|
||||||
// Don't show if product feedback is blocked by org policy (ZDR)
|
|
||||||
if (!isPolicyAllowed('allow_product_feedback')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Probability gate from GrowthBook config (separate per rating)
|
|
||||||
const probability = selected_0 === 'bad' ? badTranscriptAskConfig.probability : goodTranscriptAskConfig.probability;
|
|
||||||
return Math.random() <= probability;
|
|
||||||
}, [badTranscriptAskConfig.probability, goodTranscriptAskConfig.probability]);
|
|
||||||
const onTranscriptPromptShown = useCallback((appearanceId_1: string, surveyResponse: FeedbackSurveyResponse) => {
|
|
||||||
const trigger: TranscriptShareTrigger = surveyResponse === 'good' ? 'good_feedback_survey' : 'bad_feedback_survey';
|
|
||||||
logEvent('tengu_feedback_survey_event', {
|
|
||||||
event_type: 'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
appearance_id: appearanceId_1 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
trigger: trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
|
||||||
});
|
|
||||||
void logOTelEvent('feedback_survey', {
|
|
||||||
event_type: 'transcript_prompt_appeared',
|
|
||||||
appearance_id: appearanceId_1,
|
|
||||||
survey_type: surveyType
|
|
||||||
});
|
|
||||||
}, [surveyType]);
|
|
||||||
const onTranscriptSelect = useCallback(async (appearanceId_2: string, selected_1: TranscriptShareResponse, surveyResponse_0: FeedbackSurveyResponse | null): Promise<boolean> => {
|
|
||||||
const trigger_0: TranscriptShareTrigger = surveyResponse_0 === 'good' ? 'good_feedback_survey' : 'bad_feedback_survey';
|
|
||||||
logEvent('tengu_feedback_survey_event', {
|
|
||||||
event_type: `transcript_share_${selected_1}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
trigger: trigger_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
|
||||||
});
|
|
||||||
if (selected_1 === 'dont_ask_again') {
|
|
||||||
saveGlobalConfig(current_0 => ({
|
|
||||||
...current_0,
|
|
||||||
transcriptShareDismissed: true
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (selected_1 === 'yes') {
|
|
||||||
const result = await submitTranscriptShare(messagesRef.current, trigger_0, appearanceId_2);
|
|
||||||
logEvent('tengu_feedback_survey_event', {
|
logEvent('tengu_feedback_survey_event', {
|
||||||
event_type: (result.success ? 'transcript_share_submitted' : 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
event_type:
|
||||||
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
trigger: trigger_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
appearance_id:
|
||||||
});
|
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
return result.success;
|
last_assistant_message_id:
|
||||||
}
|
lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
return false;
|
survey_type:
|
||||||
}, [surveyType]);
|
surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
const {
|
})
|
||||||
state,
|
void logOTelEvent('feedback_survey', {
|
||||||
lastResponse,
|
event_type: 'appeared',
|
||||||
open,
|
appearance_id: appearanceId,
|
||||||
handleSelect,
|
survey_type: surveyType,
|
||||||
handleTranscriptSelect
|
})
|
||||||
} = useSurveyState({
|
},
|
||||||
hideThanksAfterMs: config.hideThanksAfterMs,
|
[updateLastShownTime, surveyType],
|
||||||
onOpen,
|
)
|
||||||
onSelect,
|
|
||||||
shouldShowTranscriptPrompt,
|
const onSelect = useCallback(
|
||||||
onTranscriptPromptShown,
|
(appearanceId: string, selected: FeedbackSurveyResponse) => {
|
||||||
onTranscriptSelect
|
updateLastShownTime(Date.now(), submitCountRef.current)
|
||||||
});
|
logEvent('tengu_feedback_survey_event', {
|
||||||
const currentModel = getMainLoopModel();
|
event_type:
|
||||||
|
'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
appearance_id:
|
||||||
|
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
response:
|
||||||
|
selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
last_assistant_message_id:
|
||||||
|
lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
survey_type:
|
||||||
|
surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
})
|
||||||
|
void logOTelEvent('feedback_survey', {
|
||||||
|
event_type: 'responded',
|
||||||
|
appearance_id: appearanceId,
|
||||||
|
response: selected,
|
||||||
|
survey_type: surveyType,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[updateLastShownTime, surveyType],
|
||||||
|
)
|
||||||
|
|
||||||
|
const shouldShowTranscriptPrompt = useCallback(
|
||||||
|
(selected: FeedbackSurveyResponse) => {
|
||||||
|
// Only bad and good ratings trigger the transcript ask
|
||||||
|
if (selected !== 'bad' && selected !== 'good') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show if user previously chose "Don't ask again"
|
||||||
|
if (getGlobalConfig().transcriptShareDismissed) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show if product feedback is blocked by org policy (ZDR)
|
||||||
|
if (!isPolicyAllowed('allow_product_feedback')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probability gate from GrowthBook config (separate per rating)
|
||||||
|
const probability =
|
||||||
|
selected === 'bad'
|
||||||
|
? badTranscriptAskConfig.probability
|
||||||
|
: goodTranscriptAskConfig.probability
|
||||||
|
return Math.random() <= probability
|
||||||
|
},
|
||||||
|
[badTranscriptAskConfig.probability, goodTranscriptAskConfig.probability],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onTranscriptPromptShown = useCallback(
|
||||||
|
(appearanceId: string, surveyResponse: FeedbackSurveyResponse) => {
|
||||||
|
const trigger: TranscriptShareTrigger =
|
||||||
|
surveyResponse === 'good'
|
||||||
|
? 'good_feedback_survey'
|
||||||
|
: 'bad_feedback_survey'
|
||||||
|
logEvent('tengu_feedback_survey_event', {
|
||||||
|
event_type:
|
||||||
|
'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
appearance_id:
|
||||||
|
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
last_assistant_message_id:
|
||||||
|
lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
survey_type:
|
||||||
|
surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
trigger:
|
||||||
|
trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
})
|
||||||
|
void logOTelEvent('feedback_survey', {
|
||||||
|
event_type: 'transcript_prompt_appeared',
|
||||||
|
appearance_id: appearanceId,
|
||||||
|
survey_type: surveyType,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[surveyType],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onTranscriptSelect = useCallback(
|
||||||
|
async (
|
||||||
|
appearanceId: string,
|
||||||
|
selected: TranscriptShareResponse,
|
||||||
|
surveyResponse: FeedbackSurveyResponse | null,
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const trigger: TranscriptShareTrigger =
|
||||||
|
surveyResponse === 'good'
|
||||||
|
? 'good_feedback_survey'
|
||||||
|
: 'bad_feedback_survey'
|
||||||
|
|
||||||
|
logEvent('tengu_feedback_survey_event', {
|
||||||
|
event_type:
|
||||||
|
`transcript_share_${selected}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
appearance_id:
|
||||||
|
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
last_assistant_message_id:
|
||||||
|
lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
survey_type:
|
||||||
|
surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
trigger:
|
||||||
|
trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (selected === 'dont_ask_again') {
|
||||||
|
saveGlobalConfig(current => ({
|
||||||
|
...current,
|
||||||
|
transcriptShareDismissed: true,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected === 'yes') {
|
||||||
|
const result = await submitTranscriptShare(
|
||||||
|
messagesRef.current,
|
||||||
|
trigger,
|
||||||
|
appearanceId,
|
||||||
|
)
|
||||||
|
logEvent('tengu_feedback_survey_event', {
|
||||||
|
event_type: (result.success
|
||||||
|
? 'transcript_share_submitted'
|
||||||
|
: 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
appearance_id:
|
||||||
|
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
trigger:
|
||||||
|
trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
})
|
||||||
|
return result.success
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
[surveyType],
|
||||||
|
)
|
||||||
|
|
||||||
|
const { state, lastResponse, open, handleSelect, handleTranscriptSelect } =
|
||||||
|
useSurveyState({
|
||||||
|
hideThanksAfterMs: config.hideThanksAfterMs,
|
||||||
|
onOpen,
|
||||||
|
onSelect,
|
||||||
|
shouldShowTranscriptPrompt,
|
||||||
|
onTranscriptPromptShown,
|
||||||
|
onTranscriptSelect,
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentModel = getMainLoopModel()
|
||||||
const isModelAllowed = useMemo(() => {
|
const isModelAllowed = useMemo(() => {
|
||||||
if (config.onForModels.length === 0) {
|
if (config.onForModels.length === 0) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if (config.onForModels.includes('*')) {
|
if (config.onForModels.includes('*')) {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
return config.onForModels.includes(currentModel);
|
return config.onForModels.includes(currentModel)
|
||||||
}, [config.onForModels, currentModel]);
|
}, [config.onForModels, currentModel])
|
||||||
|
|
||||||
const shouldOpen = useMemo(() => {
|
const shouldOpen = useMemo(() => {
|
||||||
if (state !== 'closed') {
|
if (state !== 'closed') {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't show survey when permission or ask question prompts are visible
|
// Don't show survey when permission or ask question prompts are visible
|
||||||
if (hasActivePrompt) {
|
if (hasActivePrompt) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force display for testing
|
// Force display for testing
|
||||||
if (process.env.CLAUDE_FORCE_DISPLAY_SURVEY && !feedbackSurvey.timeLastShown) {
|
if (
|
||||||
return true;
|
process.env.CLAUDE_FORCE_DISPLAY_SURVEY &&
|
||||||
|
!feedbackSurvey.timeLastShown
|
||||||
|
) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isModelAllowed) {
|
if (!isModelAllowed) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
|
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFeedbackSurveyDisabled()) {
|
if (isFeedbackSurveyDisabled()) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if product feedback is allowed by org policy
|
// Check if product feedback is allowed by org policy
|
||||||
if (!isPolicyAllowed('allow_product_feedback')) {
|
if (!isPolicyAllowed('allow_product_feedback')) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check session-local pacing
|
// Check session-local pacing
|
||||||
if (feedbackSurvey.timeLastShown) {
|
if (feedbackSurvey.timeLastShown) {
|
||||||
// Check time elapsed since last appearance in this session
|
// Check time elapsed since last appearance in this session
|
||||||
const timeSinceLastShown = Date.now() - feedbackSurvey.timeLastShown;
|
const timeSinceLastShown = Date.now() - feedbackSurvey.timeLastShown
|
||||||
if (timeSinceLastShown < config.minTimeBetweenFeedbackMs) {
|
if (timeSinceLastShown < config.minTimeBetweenFeedbackMs) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
// Check user turn requirement for subsequent appearances
|
// Check user turn requirement for subsequent appearances
|
||||||
if (feedbackSurvey.submitCountAtLastAppearance !== null && submitCount < feedbackSurvey.submitCountAtLastAppearance + config.minUserTurnsBetweenFeedback) {
|
if (
|
||||||
return false;
|
feedbackSurvey.submitCountAtLastAppearance !== null &&
|
||||||
|
submitCount <
|
||||||
|
feedbackSurvey.submitCountAtLastAppearance +
|
||||||
|
config.minUserTurnsBetweenFeedback
|
||||||
|
) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// First appearance in this session
|
// First appearance in this session
|
||||||
const timeSinceSessionStart = Date.now() - sessionStartTime.current;
|
const timeSinceSessionStart = Date.now() - sessionStartTime.current
|
||||||
if (timeSinceSessionStart < config.minTimeBeforeFeedbackMs) {
|
if (timeSinceSessionStart < config.minTimeBeforeFeedbackMs) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if (submitCount < submitCountAtSessionStart.current + config.minUserTurnsBeforeFeedback) {
|
if (
|
||||||
return false;
|
submitCount <
|
||||||
|
submitCountAtSessionStart.current + config.minUserTurnsBeforeFeedback
|
||||||
|
) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Probability check: roll once per eligibility window to avoid re-rolling
|
// Probability check: roll once per eligibility window to avoid re-rolling
|
||||||
// on every useMemo re-evaluation (which would make triggering near-certain).
|
// on every useMemo re-evaluation (which would make triggering near-certain).
|
||||||
if (lastEligibleSubmitCountRef.current !== submitCount) {
|
if (lastEligibleSubmitCountRef.current !== submitCount) {
|
||||||
lastEligibleSubmitCountRef.current = submitCount;
|
lastEligibleSubmitCountRef.current = submitCount
|
||||||
probabilityPassedRef.current = Math.random() <= (settingsRate ?? config.probability);
|
probabilityPassedRef.current =
|
||||||
|
Math.random() <= (settingsRate ?? config.probability)
|
||||||
}
|
}
|
||||||
if (!probabilityPassedRef.current) {
|
if (!probabilityPassedRef.current) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check global pacing (across all sessions)
|
// Check global pacing (across all sessions)
|
||||||
// Leave this till last because it reads from the filesystem which is expensive.
|
// Leave this till last because it reads from the filesystem which is expensive.
|
||||||
const globalFeedbackState = getGlobalConfig().feedbackSurveyState;
|
const globalFeedbackState = getGlobalConfig().feedbackSurveyState
|
||||||
if (globalFeedbackState?.lastShownTime) {
|
if (globalFeedbackState?.lastShownTime) {
|
||||||
const timeSinceGlobalLastShown = Date.now() - globalFeedbackState.lastShownTime;
|
const timeSinceGlobalLastShown =
|
||||||
|
Date.now() - globalFeedbackState.lastShownTime
|
||||||
if (timeSinceGlobalLastShown < config.minTimeBetweenGlobalFeedbackMs) {
|
if (timeSinceGlobalLastShown < config.minTimeBetweenGlobalFeedbackMs) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}, [state, isLoading, hasActivePrompt, isModelAllowed, feedbackSurvey.timeLastShown, feedbackSurvey.submitCountAtLastAppearance, submitCount, config.minTimeBetweenFeedbackMs, config.minTimeBetweenGlobalFeedbackMs, config.minUserTurnsBetweenFeedback, config.minTimeBeforeFeedbackMs, config.minUserTurnsBeforeFeedback, config.probability, settingsRate]);
|
return true
|
||||||
|
}, [
|
||||||
|
state,
|
||||||
|
isLoading,
|
||||||
|
hasActivePrompt,
|
||||||
|
isModelAllowed,
|
||||||
|
feedbackSurvey.timeLastShown,
|
||||||
|
feedbackSurvey.submitCountAtLastAppearance,
|
||||||
|
submitCount,
|
||||||
|
config.minTimeBetweenFeedbackMs,
|
||||||
|
config.minTimeBetweenGlobalFeedbackMs,
|
||||||
|
config.minUserTurnsBetweenFeedback,
|
||||||
|
config.minTimeBeforeFeedbackMs,
|
||||||
|
config.minUserTurnsBeforeFeedback,
|
||||||
|
config.probability,
|
||||||
|
settingsRate,
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldOpen) {
|
if (shouldOpen) {
|
||||||
open();
|
open()
|
||||||
}
|
}
|
||||||
}, [shouldOpen, open]);
|
}, [shouldOpen, open])
|
||||||
return {
|
|
||||||
state,
|
return { state, lastResponse, handleSelect, handleTranscriptSelect }
|
||||||
lastResponse,
|
|
||||||
handleSelect,
|
|
||||||
handleTranscriptSelect
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,212 +1,283 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js';
|
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'
|
||||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js';
|
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
||||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
import {
|
||||||
import { isAutoMemoryEnabled } from '../../memdir/paths.js';
|
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
import { isPolicyAllowed } from '../../services/policyLimits/index.js';
|
logEvent,
|
||||||
import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js';
|
} from 'src/services/analytics/index.js'
|
||||||
import type { Message } from '../../types/message.js';
|
import { isAutoMemoryEnabled } from '../../memdir/paths.js'
|
||||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
import { isPolicyAllowed } from '../../services/policyLimits/index.js'
|
||||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js'
|
||||||
import { isAutoManagedMemoryFile } from '../../utils/memoryFileDetection.js';
|
import type { Message } from '../../types/message.js'
|
||||||
import { extractTextContent, getLastAssistantMessage } from '../../utils/messages.js';
|
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||||
import { logOTelEvent } from '../../utils/telemetry/events.js';
|
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||||
import { submitTranscriptShare } from './submitTranscriptShare.js';
|
import { isAutoManagedMemoryFile } from '../../utils/memoryFileDetection.js'
|
||||||
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
|
import {
|
||||||
import { useSurveyState } from './useSurveyState.js';
|
extractTextContent,
|
||||||
import type { FeedbackSurveyResponse } from './utils.js';
|
getLastAssistantMessage,
|
||||||
const HIDE_THANKS_AFTER_MS = 3000;
|
} from '../../utils/messages.js'
|
||||||
const MEMORY_SURVEY_GATE = 'tengu_dunwich_bell';
|
import { logOTelEvent } from '../../utils/telemetry/events.js'
|
||||||
const MEMORY_SURVEY_EVENT = 'tengu_memory_survey_event';
|
import { submitTranscriptShare } from './submitTranscriptShare.js'
|
||||||
const SURVEY_PROBABILITY = 0.2;
|
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
|
||||||
const TRANSCRIPT_SHARE_TRIGGER = 'memory_survey';
|
import { useSurveyState } from './useSurveyState.js'
|
||||||
const MEMORY_WORD_RE = /\bmemor(?:y|ies)\b/i;
|
import type { FeedbackSurveyResponse } from './utils.js'
|
||||||
|
|
||||||
|
const HIDE_THANKS_AFTER_MS = 3000
|
||||||
|
const MEMORY_SURVEY_GATE = 'tengu_dunwich_bell'
|
||||||
|
const MEMORY_SURVEY_EVENT = 'tengu_memory_survey_event'
|
||||||
|
const SURVEY_PROBABILITY = 0.2
|
||||||
|
const TRANSCRIPT_SHARE_TRIGGER = 'memory_survey'
|
||||||
|
|
||||||
|
const MEMORY_WORD_RE = /\bmemor(?:y|ies)\b/i
|
||||||
|
|
||||||
function hasMemoryFileRead(messages: Message[]): boolean {
|
function hasMemoryFileRead(messages: Message[]): boolean {
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
if (message.type !== 'assistant') {
|
if (message.type !== 'assistant') {
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
const content = message.message.content;
|
const content = message.message.content
|
||||||
if (!Array.isArray(content)) {
|
if (!Array.isArray(content)) {
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
for (const block of content) {
|
for (const block of content) {
|
||||||
if (block.type !== 'tool_use' || block.name !== FILE_READ_TOOL_NAME) {
|
if (block.type !== 'tool_use' || block.name !== FILE_READ_TOOL_NAME) {
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
const input = block.input as {
|
const input = block.input as { file_path?: unknown }
|
||||||
file_path?: unknown;
|
if (
|
||||||
};
|
typeof input.file_path === 'string' &&
|
||||||
if (typeof input.file_path === 'string' && isAutoManagedMemoryFile(input.file_path)) {
|
isAutoManagedMemoryFile(input.file_path)
|
||||||
return true;
|
) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
export function useMemorySurvey(messages: Message[], isLoading: boolean, hasActivePrompt = false, {
|
|
||||||
enabled = true
|
export function useMemorySurvey(
|
||||||
}: {
|
messages: Message[],
|
||||||
enabled?: boolean;
|
isLoading: boolean,
|
||||||
} = {}): {
|
hasActivePrompt = false,
|
||||||
state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
|
{ enabled = true }: { enabled?: boolean } = {},
|
||||||
lastResponse: FeedbackSurveyResponse | null;
|
): {
|
||||||
handleSelect: (selected: FeedbackSurveyResponse) => void;
|
state:
|
||||||
handleTranscriptSelect: (selected: TranscriptShareResponse) => void;
|
| 'closed'
|
||||||
|
| 'open'
|
||||||
|
| 'thanks'
|
||||||
|
| 'transcript_prompt'
|
||||||
|
| 'submitting'
|
||||||
|
| 'submitted'
|
||||||
|
lastResponse: FeedbackSurveyResponse | null
|
||||||
|
handleSelect: (selected: FeedbackSurveyResponse) => void
|
||||||
|
handleTranscriptSelect: (selected: TranscriptShareResponse) => void
|
||||||
} {
|
} {
|
||||||
// Track assistant message UUIDs that were already evaluated so we don't
|
// Track assistant message UUIDs that were already evaluated so we don't
|
||||||
// re-roll probability on re-renders or re-scan messages for the same turn.
|
// re-roll probability on re-renders or re-scan messages for the same turn.
|
||||||
const seenAssistantUuids = useRef<Set<string>>(new Set());
|
const seenAssistantUuids = useRef<Set<string>>(new Set())
|
||||||
// Once a memory file read is observed it stays true for the session —
|
// Once a memory file read is observed it stays true for the session —
|
||||||
// skip the O(n) scan on subsequent turns.
|
// skip the O(n) scan on subsequent turns.
|
||||||
const memoryReadSeen = useRef(false);
|
const memoryReadSeen = useRef(false)
|
||||||
const messagesRef = useRef(messages);
|
const messagesRef = useRef(messages)
|
||||||
messagesRef.current = messages;
|
messagesRef.current = messages
|
||||||
|
|
||||||
const onOpen = useCallback((appearanceId: string) => {
|
const onOpen = useCallback((appearanceId: string) => {
|
||||||
logEvent(MEMORY_SURVEY_EVENT, {
|
logEvent(MEMORY_SURVEY_EVENT, {
|
||||||
event_type: 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
event_type:
|
||||||
appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
});
|
appearance_id:
|
||||||
|
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
})
|
||||||
void logOTelEvent('feedback_survey', {
|
void logOTelEvent('feedback_survey', {
|
||||||
event_type: 'appeared',
|
event_type: 'appeared',
|
||||||
appearance_id: appearanceId,
|
appearance_id: appearanceId,
|
||||||
survey_type: 'memory'
|
survey_type: 'memory',
|
||||||
});
|
})
|
||||||
}, []);
|
}, [])
|
||||||
const onSelect = useCallback((appearanceId_0: string, selected: FeedbackSurveyResponse) => {
|
|
||||||
|
const onSelect = useCallback(
|
||||||
|
(appearanceId: string, selected: FeedbackSurveyResponse) => {
|
||||||
|
logEvent(MEMORY_SURVEY_EVENT, {
|
||||||
|
event_type:
|
||||||
|
'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
appearance_id:
|
||||||
|
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
response:
|
||||||
|
selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
})
|
||||||
|
void logOTelEvent('feedback_survey', {
|
||||||
|
event_type: 'responded',
|
||||||
|
appearance_id: appearanceId,
|
||||||
|
response: selected,
|
||||||
|
survey_type: 'memory',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
const shouldShowTranscriptPrompt = useCallback(
|
||||||
|
(selected: FeedbackSurveyResponse) => {
|
||||||
|
if (process.env.USER_TYPE !== 'ant') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (selected !== 'bad' && selected !== 'good') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (getGlobalConfig().transcriptShareDismissed) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!isPolicyAllowed('allow_product_feedback')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onTranscriptPromptShown = useCallback((appearanceId: string) => {
|
||||||
logEvent(MEMORY_SURVEY_EVENT, {
|
logEvent(MEMORY_SURVEY_EVENT, {
|
||||||
event_type: 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
event_type:
|
||||||
appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
appearance_id:
|
||||||
});
|
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
void logOTelEvent('feedback_survey', {
|
trigger:
|
||||||
event_type: 'responded',
|
TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
appearance_id: appearanceId_0,
|
})
|
||||||
response: selected,
|
|
||||||
survey_type: 'memory'
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
const shouldShowTranscriptPrompt = useCallback((selected_0: FeedbackSurveyResponse) => {
|
|
||||||
if ((process.env.USER_TYPE) !== 'ant') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (selected_0 !== 'bad' && selected_0 !== 'good') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (getGlobalConfig().transcriptShareDismissed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!isPolicyAllowed('allow_product_feedback')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}, []);
|
|
||||||
const onTranscriptPromptShown = useCallback((appearanceId_1: string) => {
|
|
||||||
logEvent(MEMORY_SURVEY_EVENT, {
|
|
||||||
event_type: 'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
appearance_id: appearanceId_1 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
|
||||||
});
|
|
||||||
void logOTelEvent('feedback_survey', {
|
void logOTelEvent('feedback_survey', {
|
||||||
event_type: 'transcript_prompt_appeared',
|
event_type: 'transcript_prompt_appeared',
|
||||||
appearance_id: appearanceId_1,
|
appearance_id: appearanceId,
|
||||||
survey_type: 'memory'
|
survey_type: 'memory',
|
||||||
});
|
})
|
||||||
}, []);
|
}, [])
|
||||||
const onTranscriptSelect = useCallback(async (appearanceId_2: string, selected_1: TranscriptShareResponse): Promise<boolean> => {
|
|
||||||
logEvent(MEMORY_SURVEY_EVENT, {
|
const onTranscriptSelect = useCallback(
|
||||||
event_type: `transcript_share_${selected_1}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
async (
|
||||||
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
appearanceId: string,
|
||||||
trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
selected: TranscriptShareResponse,
|
||||||
});
|
): Promise<boolean> => {
|
||||||
if (selected_1 === 'dont_ask_again') {
|
|
||||||
saveGlobalConfig(current => ({
|
|
||||||
...current,
|
|
||||||
transcriptShareDismissed: true
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (selected_1 === 'yes') {
|
|
||||||
const result = await submitTranscriptShare(messagesRef.current, TRANSCRIPT_SHARE_TRIGGER, appearanceId_2);
|
|
||||||
logEvent(MEMORY_SURVEY_EVENT, {
|
logEvent(MEMORY_SURVEY_EVENT, {
|
||||||
event_type: (result.success ? 'transcript_share_submitted' : 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
event_type:
|
||||||
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
`transcript_share_${selected}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
appearance_id:
|
||||||
});
|
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
return result.success;
|
trigger:
|
||||||
}
|
TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
return false;
|
})
|
||||||
}, []);
|
|
||||||
const {
|
if (selected === 'dont_ask_again') {
|
||||||
state,
|
saveGlobalConfig(current => ({
|
||||||
lastResponse,
|
...current,
|
||||||
open,
|
transcriptShareDismissed: true,
|
||||||
handleSelect,
|
}))
|
||||||
handleTranscriptSelect
|
}
|
||||||
} = useSurveyState({
|
|
||||||
hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
|
if (selected === 'yes') {
|
||||||
onOpen,
|
const result = await submitTranscriptShare(
|
||||||
onSelect,
|
messagesRef.current,
|
||||||
shouldShowTranscriptPrompt,
|
TRANSCRIPT_SHARE_TRIGGER,
|
||||||
onTranscriptPromptShown,
|
appearanceId,
|
||||||
onTranscriptSelect
|
)
|
||||||
});
|
logEvent(MEMORY_SURVEY_EVENT, {
|
||||||
const lastAssistant = useMemo(() => getLastAssistantMessage(messages), [messages]);
|
event_type: (result.success
|
||||||
|
? 'transcript_share_submitted'
|
||||||
|
: 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
appearance_id:
|
||||||
|
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
trigger:
|
||||||
|
TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
})
|
||||||
|
return result.success
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
const { state, lastResponse, open, handleSelect, handleTranscriptSelect } =
|
||||||
|
useSurveyState({
|
||||||
|
hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
|
||||||
|
onOpen,
|
||||||
|
onSelect,
|
||||||
|
shouldShowTranscriptPrompt,
|
||||||
|
onTranscriptPromptShown,
|
||||||
|
onTranscriptSelect,
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastAssistant = useMemo(
|
||||||
|
() => getLastAssistantMessage(messages),
|
||||||
|
[messages],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return;
|
if (!enabled) return
|
||||||
|
|
||||||
// /clear resets messages but REPL stays mounted — reset refs so a memory
|
// /clear resets messages but REPL stays mounted — reset refs so a memory
|
||||||
// read from the previous conversation doesn't leak into the new one.
|
// read from the previous conversation doesn't leak into the new one.
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
memoryReadSeen.current = false;
|
memoryReadSeen.current = false
|
||||||
seenAssistantUuids.current.clear();
|
seenAssistantUuids.current.clear()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state !== 'closed' || isLoading || hasActivePrompt) {
|
if (state !== 'closed' || isLoading || hasActivePrompt) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3P default: survey off (no GrowthBook on Bedrock/Vertex/Foundry).
|
// 3P default: survey off (no GrowthBook on Bedrock/Vertex/Foundry).
|
||||||
if (!getFeatureValue_CACHED_MAY_BE_STALE(MEMORY_SURVEY_GATE, false)) {
|
if (!getFeatureValue_CACHED_MAY_BE_STALE(MEMORY_SURVEY_GATE, false)) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAutoMemoryEnabled()) {
|
if (!isAutoMemoryEnabled()) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFeedbackSurveyDisabled()) {
|
if (isFeedbackSurveyDisabled()) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPolicyAllowed('allow_product_feedback')) {
|
if (!isPolicyAllowed('allow_product_feedback')) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
|
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lastAssistant || seenAssistantUuids.current.has(lastAssistant.uuid)) {
|
if (!lastAssistant || seenAssistantUuids.current.has(lastAssistant.uuid)) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const text = extractTextContent(Array.isArray(lastAssistant.message.content) ? lastAssistant.message.content : [], ' ');
|
|
||||||
|
const text = extractTextContent(lastAssistant.message.content, ' ')
|
||||||
if (!MEMORY_WORD_RE.test(text)) {
|
if (!MEMORY_WORD_RE.test(text)) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as evaluated before the memory-read scan so a turn that mentions
|
// Mark as evaluated before the memory-read scan so a turn that mentions
|
||||||
// "memory" but has no memory read doesn't trigger repeated O(n) scans
|
// "memory" but has no memory read doesn't trigger repeated O(n) scans
|
||||||
// on subsequent renders with the same last assistant message.
|
// on subsequent renders with the same last assistant message.
|
||||||
seenAssistantUuids.current.add(lastAssistant.uuid);
|
seenAssistantUuids.current.add(lastAssistant.uuid)
|
||||||
|
|
||||||
if (!memoryReadSeen.current) {
|
if (!memoryReadSeen.current) {
|
||||||
memoryReadSeen.current = hasMemoryFileRead(messages);
|
memoryReadSeen.current = hasMemoryFileRead(messages)
|
||||||
}
|
}
|
||||||
if (!memoryReadSeen.current) {
|
if (!memoryReadSeen.current) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Math.random() < SURVEY_PROBABILITY) {
|
if (Math.random() < SURVEY_PROBABILITY) {
|
||||||
open();
|
open()
|
||||||
}
|
}
|
||||||
}, [enabled, state, isLoading, hasActivePrompt, lastAssistant, messages, open]);
|
}, [
|
||||||
return {
|
enabled,
|
||||||
state,
|
state,
|
||||||
lastResponse,
|
isLoading,
|
||||||
handleSelect,
|
hasActivePrompt,
|
||||||
handleTranscriptSelect
|
lastAssistant,
|
||||||
};
|
messages,
|
||||||
|
open,
|
||||||
|
])
|
||||||
|
|
||||||
|
return { state, lastResponse, handleSelect, handleTranscriptSelect }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,205 +1,195 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'
|
||||||
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js';
|
import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
||||||
import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js';
|
import {
|
||||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
import { shouldUseSessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js';
|
logEvent,
|
||||||
import type { Message } from '../../types/message.js';
|
} from 'src/services/analytics/index.js'
|
||||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
import { shouldUseSessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js'
|
||||||
import { isCompactBoundaryMessage } from '../../utils/messages.js';
|
import type { Message } from '../../types/message.js'
|
||||||
import { logOTelEvent } from '../../utils/telemetry/events.js';
|
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||||
import { useSurveyState } from './useSurveyState.js';
|
import { isCompactBoundaryMessage } from '../../utils/messages.js'
|
||||||
import type { FeedbackSurveyResponse } from './utils.js';
|
import { logOTelEvent } from '../../utils/telemetry/events.js'
|
||||||
const HIDE_THANKS_AFTER_MS = 3000;
|
import { useSurveyState } from './useSurveyState.js'
|
||||||
const POST_COMPACT_SURVEY_GATE = 'tengu_post_compact_survey';
|
import type { FeedbackSurveyResponse } from './utils.js'
|
||||||
const SURVEY_PROBABILITY = 0.2; // Show survey 20% of the time after compaction
|
|
||||||
|
|
||||||
function hasMessageAfterBoundary(messages: Message[], boundaryUuid: string): boolean {
|
const HIDE_THANKS_AFTER_MS = 3000
|
||||||
const boundaryIndex = messages.findIndex(msg => msg.uuid === boundaryUuid);
|
const POST_COMPACT_SURVEY_GATE = 'tengu_post_compact_survey'
|
||||||
|
const SURVEY_PROBABILITY = 0.2 // Show survey 20% of the time after compaction
|
||||||
|
|
||||||
|
function hasMessageAfterBoundary(
|
||||||
|
messages: Message[],
|
||||||
|
boundaryUuid: string,
|
||||||
|
): boolean {
|
||||||
|
const boundaryIndex = messages.findIndex(msg => msg.uuid === boundaryUuid)
|
||||||
if (boundaryIndex === -1) {
|
if (boundaryIndex === -1) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there's a user or assistant message after the boundary
|
// Check if there's a user or assistant message after the boundary
|
||||||
for (let i = boundaryIndex + 1; i < messages.length; i++) {
|
for (let i = boundaryIndex + 1; i < messages.length; i++) {
|
||||||
const msg = messages[i];
|
const msg = messages[i]
|
||||||
if (msg && (msg.type === 'user' || msg.type === 'assistant')) {
|
if (msg && (msg.type === 'user' || msg.type === 'assistant')) {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
export function usePostCompactSurvey(messages, isLoading, t0, t1) {
|
|
||||||
const $ = _c(23);
|
export function usePostCompactSurvey(
|
||||||
const hasActivePrompt = t0 === undefined ? false : t0;
|
messages: Message[],
|
||||||
let t2;
|
isLoading: boolean,
|
||||||
if ($[0] !== t1) {
|
hasActivePrompt = false,
|
||||||
t2 = t1 === undefined ? {} : t1;
|
{ enabled = true }: { enabled?: boolean } = {},
|
||||||
$[0] = t1;
|
): {
|
||||||
$[1] = t2;
|
state:
|
||||||
} else {
|
| 'closed'
|
||||||
t2 = $[1];
|
| 'open'
|
||||||
}
|
| 'thanks'
|
||||||
const {
|
| 'transcript_prompt'
|
||||||
enabled: t3
|
| 'submitting'
|
||||||
} = t2;
|
| 'submitted'
|
||||||
const enabled = t3 === undefined ? true : t3;
|
lastResponse: FeedbackSurveyResponse | null
|
||||||
const [gateEnabled, setGateEnabled] = useState(null);
|
handleSelect: (selected: FeedbackSurveyResponse) => void
|
||||||
let t4;
|
} {
|
||||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
const [gateEnabled, setGateEnabled] = useState<boolean | null>(null)
|
||||||
t4 = new Set();
|
const seenCompactBoundaries = useRef<Set<string>>(new Set())
|
||||||
$[2] = t4;
|
// Track the compact boundary we're waiting on (to show survey after next message)
|
||||||
} else {
|
const pendingCompactBoundaryUuid = useRef<string | null>(null)
|
||||||
t4 = $[2];
|
|
||||||
}
|
const onOpen = useCallback((appearanceId: string) => {
|
||||||
const seenCompactBoundaries = useRef(t4);
|
const smCompactionEnabled = shouldUseSessionMemoryCompaction()
|
||||||
const pendingCompactBoundaryUuid = useRef(null);
|
logEvent('tengu_post_compact_survey_event', {
|
||||||
const onOpen = _temp;
|
event_type:
|
||||||
const onSelect = _temp2;
|
'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
let t5;
|
appearance_id:
|
||||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
t5 = {
|
session_memory_compaction_enabled:
|
||||||
hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
|
smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
onOpen,
|
})
|
||||||
onSelect
|
void logOTelEvent('feedback_survey', {
|
||||||
};
|
event_type: 'appeared',
|
||||||
$[3] = t5;
|
appearance_id: appearanceId,
|
||||||
} else {
|
survey_type: 'post_compact',
|
||||||
t5 = $[3];
|
})
|
||||||
}
|
}, [])
|
||||||
const {
|
|
||||||
state,
|
const onSelect = useCallback(
|
||||||
lastResponse,
|
(appearanceId: string, selected: FeedbackSurveyResponse) => {
|
||||||
open,
|
const smCompactionEnabled = shouldUseSessionMemoryCompaction()
|
||||||
handleSelect
|
logEvent('tengu_post_compact_survey_event', {
|
||||||
} = useSurveyState(t5);
|
event_type:
|
||||||
let t6;
|
'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
let t7;
|
appearance_id:
|
||||||
if ($[4] !== enabled) {
|
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
t6 = () => {
|
response:
|
||||||
if (!enabled) {
|
selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
return;
|
session_memory_compaction_enabled:
|
||||||
}
|
smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
setGateEnabled(checkStatsigFeatureGate_CACHED_MAY_BE_STALE(POST_COMPACT_SURVEY_GATE));
|
})
|
||||||
};
|
void logOTelEvent('feedback_survey', {
|
||||||
t7 = [enabled];
|
event_type: 'responded',
|
||||||
$[4] = enabled;
|
appearance_id: appearanceId,
|
||||||
$[5] = t6;
|
response: selected,
|
||||||
$[6] = t7;
|
survey_type: 'post_compact',
|
||||||
} else {
|
})
|
||||||
t6 = $[5];
|
},
|
||||||
t7 = $[6];
|
[],
|
||||||
}
|
)
|
||||||
useEffect(t6, t7);
|
|
||||||
let t8;
|
const { state, lastResponse, open, handleSelect } = useSurveyState({
|
||||||
if ($[7] !== messages) {
|
hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
|
||||||
t8 = new Set(messages.filter(_temp3).map(_temp4));
|
onOpen,
|
||||||
$[7] = messages;
|
onSelect,
|
||||||
$[8] = t8;
|
})
|
||||||
} else {
|
|
||||||
t8 = $[8];
|
// Check the feature gate on mount
|
||||||
}
|
useEffect(() => {
|
||||||
const currentCompactBoundaries = t8;
|
if (!enabled) return
|
||||||
let t10;
|
setGateEnabled(
|
||||||
let t9;
|
checkStatsigFeatureGate_CACHED_MAY_BE_STALE(POST_COMPACT_SURVEY_GATE),
|
||||||
if ($[9] !== currentCompactBoundaries || $[10] !== enabled || $[11] !== gateEnabled || $[12] !== hasActivePrompt || $[13] !== isLoading || $[14] !== messages || $[15] !== open || $[16] !== state) {
|
)
|
||||||
t9 = () => {
|
}, [enabled])
|
||||||
if (!enabled) {
|
|
||||||
return;
|
// Find compact boundary messages
|
||||||
}
|
const currentCompactBoundaries = useMemo(
|
||||||
if (state !== "closed" || isLoading) {
|
() =>
|
||||||
return;
|
new Set(
|
||||||
}
|
messages
|
||||||
if (hasActivePrompt) {
|
.filter(msg => isCompactBoundaryMessage(msg))
|
||||||
return;
|
.map(msg => msg.uuid),
|
||||||
}
|
),
|
||||||
if (gateEnabled !== true) {
|
[messages],
|
||||||
return;
|
)
|
||||||
}
|
|
||||||
if (isFeedbackSurveyDisabled()) {
|
// Detect new compact boundaries and defer showing survey until next message
|
||||||
return;
|
useEffect(() => {
|
||||||
}
|
if (!enabled) return
|
||||||
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
|
|
||||||
return;
|
// Don't process if already showing
|
||||||
}
|
if (state !== 'closed' || isLoading) {
|
||||||
if (pendingCompactBoundaryUuid.current !== null) {
|
return
|
||||||
if (hasMessageAfterBoundary(messages, pendingCompactBoundaryUuid.current)) {
|
}
|
||||||
pendingCompactBoundaryUuid.current = null;
|
|
||||||
if (Math.random() < SURVEY_PROBABILITY) {
|
// Don't show survey when permission or ask question prompts are visible
|
||||||
open();
|
if (hasActivePrompt) {
|
||||||
}
|
return
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
// Check if the gate is enabled
|
||||||
|
if (gateEnabled !== true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFeedbackSurveyDisabled()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if survey is explicitly disabled
|
||||||
|
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, check if we have a pending compact and a new message has arrived
|
||||||
|
if (pendingCompactBoundaryUuid.current !== null) {
|
||||||
|
if (
|
||||||
|
hasMessageAfterBoundary(messages, pendingCompactBoundaryUuid.current)
|
||||||
|
) {
|
||||||
|
// A new message arrived after the compact - decide whether to show survey
|
||||||
|
pendingCompactBoundaryUuid.current = null
|
||||||
|
|
||||||
|
// Only show survey 20% of the time
|
||||||
|
if (Math.random() < SURVEY_PROBABILITY) {
|
||||||
|
open()
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
const newBoundaries = Array.from(currentCompactBoundaries).filter(uuid => !seenCompactBoundaries.current.has(uuid));
|
}
|
||||||
if (newBoundaries.length > 0) {
|
|
||||||
seenCompactBoundaries.current = new Set(currentCompactBoundaries);
|
// Find new compact boundaries that we haven't seen yet
|
||||||
pendingCompactBoundaryUuid.current = newBoundaries[newBoundaries.length - 1];
|
const newBoundaries = Array.from(currentCompactBoundaries).filter(
|
||||||
}
|
uuid => !seenCompactBoundaries.current.has(uuid),
|
||||||
};
|
)
|
||||||
t10 = [enabled, currentCompactBoundaries, state, isLoading, hasActivePrompt, gateEnabled, messages, open];
|
|
||||||
$[9] = currentCompactBoundaries;
|
if (newBoundaries.length > 0) {
|
||||||
$[10] = enabled;
|
// Mark these boundaries as seen
|
||||||
$[11] = gateEnabled;
|
seenCompactBoundaries.current = new Set(currentCompactBoundaries)
|
||||||
$[12] = hasActivePrompt;
|
|
||||||
$[13] = isLoading;
|
// Don't show survey immediately - wait for next message
|
||||||
$[14] = messages;
|
// Store the most recent new boundary UUID
|
||||||
$[15] = open;
|
pendingCompactBoundaryUuid.current =
|
||||||
$[16] = state;
|
newBoundaries[newBoundaries.length - 1]!
|
||||||
$[17] = t10;
|
}
|
||||||
$[18] = t9;
|
}, [
|
||||||
} else {
|
enabled,
|
||||||
t10 = $[17];
|
currentCompactBoundaries,
|
||||||
t9 = $[18];
|
state,
|
||||||
}
|
isLoading,
|
||||||
useEffect(t9, t10);
|
hasActivePrompt,
|
||||||
let t11;
|
gateEnabled,
|
||||||
if ($[19] !== handleSelect || $[20] !== lastResponse || $[21] !== state) {
|
messages,
|
||||||
t11 = {
|
open,
|
||||||
state,
|
])
|
||||||
lastResponse,
|
|
||||||
handleSelect
|
return { state, lastResponse, handleSelect }
|
||||||
};
|
|
||||||
$[19] = handleSelect;
|
|
||||||
$[20] = lastResponse;
|
|
||||||
$[21] = state;
|
|
||||||
$[22] = t11;
|
|
||||||
} else {
|
|
||||||
t11 = $[22];
|
|
||||||
}
|
|
||||||
return t11;
|
|
||||||
}
|
|
||||||
function _temp4(msg_0) {
|
|
||||||
return msg_0.uuid;
|
|
||||||
}
|
|
||||||
function _temp3(msg) {
|
|
||||||
return isCompactBoundaryMessage(msg);
|
|
||||||
}
|
|
||||||
function _temp2(appearanceId_0, selected) {
|
|
||||||
const smCompactionEnabled_0 = shouldUseSessionMemoryCompaction();
|
|
||||||
logEvent("tengu_post_compact_survey_event", {
|
|
||||||
event_type: "responded" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
session_memory_compaction_enabled: smCompactionEnabled_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
|
||||||
});
|
|
||||||
logOTelEvent("feedback_survey", {
|
|
||||||
event_type: "responded",
|
|
||||||
appearance_id: appearanceId_0,
|
|
||||||
response: selected,
|
|
||||||
survey_type: "post_compact"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function _temp(appearanceId) {
|
|
||||||
const smCompactionEnabled = shouldUseSessionMemoryCompaction();
|
|
||||||
logEvent("tengu_post_compact_survey_event", {
|
|
||||||
event_type: "appeared" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
||||||
session_memory_compaction_enabled: smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
|
||||||
});
|
|
||||||
logOTelEvent("feedback_survey", {
|
|
||||||
event_type: "appeared",
|
|
||||||
appearance_id: appearanceId,
|
|
||||||
survey_type: "post_compact"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,99 +1,144 @@
|
|||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto'
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useRef, useState } from 'react'
|
||||||
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
|
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
|
||||||
import type { FeedbackSurveyResponse } from './utils.js';
|
import type { FeedbackSurveyResponse } from './utils.js'
|
||||||
type SurveyState = 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
|
|
||||||
|
type SurveyState =
|
||||||
|
| 'closed'
|
||||||
|
| 'open'
|
||||||
|
| 'thanks'
|
||||||
|
| 'transcript_prompt'
|
||||||
|
| 'submitting'
|
||||||
|
| 'submitted'
|
||||||
|
|
||||||
type UseSurveyStateOptions = {
|
type UseSurveyStateOptions = {
|
||||||
hideThanksAfterMs: number;
|
hideThanksAfterMs: number
|
||||||
onOpen: (appearanceId: string) => void | Promise<void>;
|
onOpen: (appearanceId: string) => void | Promise<void>
|
||||||
onSelect: (appearanceId: string, selected: FeedbackSurveyResponse) => void | Promise<void>;
|
onSelect: (
|
||||||
shouldShowTranscriptPrompt?: (selected: FeedbackSurveyResponse) => boolean;
|
appearanceId: string,
|
||||||
onTranscriptPromptShown?: (appearanceId: string, surveyResponse: FeedbackSurveyResponse) => void;
|
selected: FeedbackSurveyResponse,
|
||||||
onTranscriptSelect?: (appearanceId: string, selected: TranscriptShareResponse, surveyResponse: FeedbackSurveyResponse | null) => boolean | Promise<boolean>;
|
) => void | Promise<void>
|
||||||
};
|
shouldShowTranscriptPrompt?: (selected: FeedbackSurveyResponse) => boolean
|
||||||
|
onTranscriptPromptShown?: (
|
||||||
|
appearanceId: string,
|
||||||
|
surveyResponse: FeedbackSurveyResponse,
|
||||||
|
) => void
|
||||||
|
onTranscriptSelect?: (
|
||||||
|
appearanceId: string,
|
||||||
|
selected: TranscriptShareResponse,
|
||||||
|
surveyResponse: FeedbackSurveyResponse | null,
|
||||||
|
) => boolean | Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
export function useSurveyState({
|
export function useSurveyState({
|
||||||
hideThanksAfterMs,
|
hideThanksAfterMs,
|
||||||
onOpen,
|
onOpen,
|
||||||
onSelect,
|
onSelect,
|
||||||
shouldShowTranscriptPrompt,
|
shouldShowTranscriptPrompt,
|
||||||
onTranscriptPromptShown,
|
onTranscriptPromptShown,
|
||||||
onTranscriptSelect
|
onTranscriptSelect,
|
||||||
}: UseSurveyStateOptions): {
|
}: UseSurveyStateOptions): {
|
||||||
state: SurveyState;
|
state: SurveyState
|
||||||
lastResponse: FeedbackSurveyResponse | null;
|
lastResponse: FeedbackSurveyResponse | null
|
||||||
open: () => void;
|
open: () => void
|
||||||
handleSelect: (selected: FeedbackSurveyResponse) => boolean;
|
handleSelect: (selected: FeedbackSurveyResponse) => boolean
|
||||||
handleTranscriptSelect: (selected: TranscriptShareResponse) => void;
|
handleTranscriptSelect: (selected: TranscriptShareResponse) => void
|
||||||
} {
|
} {
|
||||||
const [state, setState] = useState<SurveyState>('closed');
|
const [state, setState] = useState<SurveyState>('closed')
|
||||||
const [lastResponse, setLastResponse] = useState<FeedbackSurveyResponse | null>(null);
|
const [lastResponse, setLastResponse] =
|
||||||
const appearanceId = useRef(randomUUID());
|
useState<FeedbackSurveyResponse | null>(null)
|
||||||
const lastResponseRef = useRef<FeedbackSurveyResponse | null>(null);
|
const appearanceId = useRef(randomUUID())
|
||||||
|
const lastResponseRef = useRef<FeedbackSurveyResponse | null>(null)
|
||||||
|
|
||||||
const showThanksThenClose = useCallback(() => {
|
const showThanksThenClose = useCallback(() => {
|
||||||
setState('thanks');
|
setState('thanks')
|
||||||
setTimeout((setState_0, setLastResponse_0) => {
|
setTimeout(
|
||||||
setState_0('closed');
|
(setState, setLastResponse) => {
|
||||||
setLastResponse_0(null);
|
setState('closed')
|
||||||
}, hideThanksAfterMs, setState, setLastResponse);
|
setLastResponse(null)
|
||||||
}, [hideThanksAfterMs]);
|
},
|
||||||
|
hideThanksAfterMs,
|
||||||
|
setState,
|
||||||
|
setLastResponse,
|
||||||
|
)
|
||||||
|
}, [hideThanksAfterMs])
|
||||||
|
|
||||||
const showSubmittedThenClose = useCallback(() => {
|
const showSubmittedThenClose = useCallback(() => {
|
||||||
setState('submitted');
|
setState('submitted')
|
||||||
setTimeout(setState, hideThanksAfterMs, 'closed');
|
setTimeout(setState, hideThanksAfterMs, 'closed')
|
||||||
}, [hideThanksAfterMs]);
|
}, [hideThanksAfterMs])
|
||||||
|
|
||||||
const open = useCallback(() => {
|
const open = useCallback(() => {
|
||||||
if (state !== 'closed') {
|
if (state !== 'closed') {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
setState('open');
|
setState('open')
|
||||||
appearanceId.current = randomUUID();
|
appearanceId.current = randomUUID()
|
||||||
void onOpen(appearanceId.current);
|
void onOpen(appearanceId.current)
|
||||||
}, [state, onOpen]);
|
}, [state, onOpen])
|
||||||
const handleSelect = useCallback((selected: FeedbackSurveyResponse): boolean => {
|
|
||||||
setLastResponse(selected);
|
const handleSelect = useCallback(
|
||||||
lastResponseRef.current = selected;
|
(selected: FeedbackSurveyResponse): boolean => {
|
||||||
// Always fire the survey response event first
|
setLastResponse(selected)
|
||||||
void onSelect(appearanceId.current, selected);
|
lastResponseRef.current = selected
|
||||||
if (selected === 'dismissed') {
|
// Always fire the survey response event first
|
||||||
setState('closed');
|
void onSelect(appearanceId.current, selected)
|
||||||
setLastResponse(null);
|
|
||||||
} else if (shouldShowTranscriptPrompt?.(selected)) {
|
if (selected === 'dismissed') {
|
||||||
setState('transcript_prompt');
|
setState('closed')
|
||||||
onTranscriptPromptShown?.(appearanceId.current, selected);
|
setLastResponse(null)
|
||||||
return true;
|
} else if (shouldShowTranscriptPrompt?.(selected)) {
|
||||||
} else {
|
setState('transcript_prompt')
|
||||||
showThanksThenClose();
|
onTranscriptPromptShown?.(appearanceId.current, selected)
|
||||||
}
|
return true
|
||||||
return false;
|
} else {
|
||||||
}, [showThanksThenClose, onSelect, shouldShowTranscriptPrompt, onTranscriptPromptShown]);
|
showThanksThenClose()
|
||||||
const handleTranscriptSelect = useCallback((selected_0: TranscriptShareResponse) => {
|
}
|
||||||
switch (selected_0) {
|
return false
|
||||||
case 'yes':
|
},
|
||||||
setState('submitting');
|
[
|
||||||
void (async () => {
|
showThanksThenClose,
|
||||||
try {
|
onSelect,
|
||||||
const success = await onTranscriptSelect?.(appearanceId.current, selected_0, lastResponseRef.current);
|
shouldShowTranscriptPrompt,
|
||||||
if (success) {
|
onTranscriptPromptShown,
|
||||||
showSubmittedThenClose();
|
],
|
||||||
} else {
|
)
|
||||||
showThanksThenClose();
|
|
||||||
|
const handleTranscriptSelect = useCallback(
|
||||||
|
(selected: TranscriptShareResponse) => {
|
||||||
|
switch (selected) {
|
||||||
|
case 'yes':
|
||||||
|
setState('submitting')
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const success = await onTranscriptSelect?.(
|
||||||
|
appearanceId.current,
|
||||||
|
selected,
|
||||||
|
lastResponseRef.current,
|
||||||
|
)
|
||||||
|
if (success) {
|
||||||
|
showSubmittedThenClose()
|
||||||
|
} else {
|
||||||
|
showThanksThenClose()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showThanksThenClose()
|
||||||
}
|
}
|
||||||
} catch {
|
})()
|
||||||
showThanksThenClose();
|
break
|
||||||
}
|
case 'no':
|
||||||
})();
|
case 'dont_ask_again':
|
||||||
break;
|
void onTranscriptSelect?.(
|
||||||
case 'no':
|
appearanceId.current,
|
||||||
case 'dont_ask_again':
|
selected,
|
||||||
void onTranscriptSelect?.(appearanceId.current, selected_0, lastResponseRef.current);
|
lastResponseRef.current,
|
||||||
showThanksThenClose();
|
)
|
||||||
break;
|
showThanksThenClose()
|
||||||
}
|
break
|
||||||
}, [showThanksThenClose, showSubmittedThenClose, onTranscriptSelect]);
|
}
|
||||||
return {
|
},
|
||||||
state,
|
[showThanksThenClose, showSubmittedThenClose, onTranscriptSelect],
|
||||||
lastResponse,
|
)
|
||||||
open,
|
|
||||||
handleSelect,
|
return { state, lastResponse, open, handleSelect, handleTranscriptSelect }
|
||||||
handleTranscriptSelect
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,38 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { stringWidth } from '../../ink/stringWidth.js'
|
||||||
import { stringWidth } from '../../ink/stringWidth.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import TextInput from '../TextInput.js'
|
||||||
import TextInput from '../TextInput.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string;
|
value: string
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void
|
||||||
historyFailedMatch: boolean;
|
historyFailedMatch: boolean
|
||||||
};
|
|
||||||
function HistorySearchInput(t0) {
|
|
||||||
const $ = _c(9);
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
historyFailedMatch
|
|
||||||
} = t0;
|
|
||||||
const t1 = historyFailedMatch ? "no matching prompt:" : "search prompts:";
|
|
||||||
let t2;
|
|
||||||
if ($[0] !== t1) {
|
|
||||||
t2 = <Text dimColor={true}>{t1}</Text>;
|
|
||||||
$[0] = t1;
|
|
||||||
$[1] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[1];
|
|
||||||
}
|
|
||||||
const t3 = stringWidth(value) + 1;
|
|
||||||
let t4;
|
|
||||||
if ($[2] !== onChange || $[3] !== t3 || $[4] !== value) {
|
|
||||||
t4 = <TextInput value={value} onChange={onChange} cursorOffset={value.length} onChangeCursorOffset={_temp} columns={t3} focus={true} showCursor={true} multiline={false} dimColor={true} />;
|
|
||||||
$[2] = onChange;
|
|
||||||
$[3] = t3;
|
|
||||||
$[4] = value;
|
|
||||||
$[5] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[5];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[6] !== t2 || $[7] !== t4) {
|
|
||||||
t5 = <Box gap={1}>{t2}{t4}</Box>;
|
|
||||||
$[6] = t2;
|
|
||||||
$[7] = t4;
|
|
||||||
$[8] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[8];
|
|
||||||
}
|
|
||||||
return t5;
|
|
||||||
}
|
}
|
||||||
function _temp() {}
|
|
||||||
export default HistorySearchInput;
|
function HistorySearchInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
historyFailedMatch,
|
||||||
|
}: Props): React.ReactNode {
|
||||||
|
return (
|
||||||
|
<Box gap={1}>
|
||||||
|
<Text dimColor>
|
||||||
|
{historyFailedMatch ? 'no matching prompt:' : 'search prompts:'}
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
// Force cursor to end of search input since navigation should cancel search
|
||||||
|
cursorOffset={value.length}
|
||||||
|
onChangeCursorOffset={() => {}}
|
||||||
|
columns={stringWidth(value) + 1}
|
||||||
|
focus={true}
|
||||||
|
showCursor={true}
|
||||||
|
multiline={false}
|
||||||
|
dimColor={true}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistorySearchInput
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react'
|
||||||
import { FLAG_ICON } from '../../constants/figures.js';
|
import { FLAG_ICON } from '../../constants/figures.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ANT-ONLY: Banner shown in the transcript that prompts users to report
|
* ANT-ONLY: Banner shown in the transcript that prompts users to report
|
||||||
* issues via /issue. Appears when friction is detected in the conversation.
|
* issues via /issue. Appears when friction is detected in the conversation.
|
||||||
*/
|
*/
|
||||||
export function IssueFlagBanner() {
|
export function IssueFlagBanner(): React.ReactNode {
|
||||||
return null;
|
if (process.env.USER_TYPE !== 'ant') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="row" marginTop={1} width="100%">
|
||||||
|
<Box minWidth={2}>
|
||||||
|
<Text color="warning">{FLAG_ICON}</Text>
|
||||||
|
</Box>
|
||||||
|
<Text>
|
||||||
|
<Text dimColor>[ANT-ONLY] </Text>
|
||||||
|
<Text color="warning" bold>
|
||||||
|
Something off with Claude?
|
||||||
|
</Text>
|
||||||
|
<Text dimColor> /issue to report it</Text>
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,218 +1,201 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import { feature } from 'bun:bundle'
|
||||||
import { feature } from 'bun:bundle';
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { type ReactNode, useEffect, useMemo, useState } from 'react'
|
||||||
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
import {
|
||||||
import { type Notification, useNotifications } from 'src/context/notifications.js';
|
type Notification,
|
||||||
import { logEvent } from 'src/services/analytics/index.js';
|
useNotifications,
|
||||||
import { useAppState } from 'src/state/AppState.js';
|
} from 'src/context/notifications.js'
|
||||||
import { useVoiceState } from '../../context/voice.js';
|
import { logEvent } from 'src/services/analytics/index.js'
|
||||||
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js';
|
import { useAppState } from 'src/state/AppState.js'
|
||||||
import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js';
|
import { useVoiceState } from '../../context/voice.js'
|
||||||
import type { IDESelection } from '../../hooks/useIdeSelection.js';
|
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
|
||||||
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
|
import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js'
|
||||||
import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js';
|
import type { IDESelection } from '../../hooks/useIdeSelection.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
|
||||||
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js';
|
import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'
|
||||||
import { calculateTokenWarningState } from '../../services/compact/autoCompact.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import type { MCPServerConnection } from '../../services/mcp/types.js';
|
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'
|
||||||
import type { Message } from '../../types/message.js';
|
import { calculateTokenWarningState } from '../../services/compact/autoCompact.js'
|
||||||
import { getApiKeyHelperElapsedMs, getConfiguredApiKeyHelper, getSubscriptionType } from '../../utils/auth.js';
|
import type { MCPServerConnection } from '../../services/mcp/types.js'
|
||||||
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js';
|
import type { Message } from '../../types/message.js'
|
||||||
import { getExternalEditor } from '../../utils/editor.js';
|
import {
|
||||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
getApiKeyHelperElapsedMs,
|
||||||
import { formatDuration } from '../../utils/format.js';
|
getConfiguredApiKeyHelper,
|
||||||
import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js';
|
getSubscriptionType,
|
||||||
import { toIDEDisplayName } from '../../utils/ide.js';
|
} from '../../utils/auth.js'
|
||||||
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js';
|
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
|
||||||
import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js';
|
import { getExternalEditor } from '../../utils/editor.js'
|
||||||
import { AutoUpdaterWrapper } from '../AutoUpdaterWrapper.js';
|
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
import { formatDuration } from '../../utils/format.js'
|
||||||
import { IdeStatusIndicator } from '../IdeStatusIndicator.js';
|
import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js'
|
||||||
import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js';
|
import { toIDEDisplayName } from '../../utils/ide.js'
|
||||||
import { SentryErrorBoundary } from '../SentryErrorBoundary.js';
|
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'
|
||||||
import { TokenWarning } from '../TokenWarning.js';
|
import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js'
|
||||||
import { SandboxPromptFooterHint } from './SandboxPromptFooterHint.js';
|
import { AutoUpdaterWrapper } from '../AutoUpdaterWrapper.js'
|
||||||
|
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||||
|
import { IdeStatusIndicator } from '../IdeStatusIndicator.js'
|
||||||
|
import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js'
|
||||||
|
import { SentryErrorBoundary } from '../SentryErrorBoundary.js'
|
||||||
|
import { TokenWarning } from '../TokenWarning.js'
|
||||||
|
import { SandboxPromptFooterHint } from './SandboxPromptFooterHint.js'
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
const VoiceIndicator: typeof import('./VoiceIndicator.js').VoiceIndicator = feature('VOICE_MODE') ? require('./VoiceIndicator.js').VoiceIndicator : () => null;
|
const VoiceIndicator: typeof import('./VoiceIndicator.js').VoiceIndicator =
|
||||||
|
feature('VOICE_MODE')
|
||||||
|
? require('./VoiceIndicator.js').VoiceIndicator
|
||||||
|
: () => null
|
||||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||||
|
|
||||||
export const FOOTER_TEMPORARY_STATUS_TIMEOUT = 5000;
|
export const FOOTER_TEMPORARY_STATUS_TIMEOUT = 5000
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
apiKeyStatus: VerificationStatus;
|
apiKeyStatus: VerificationStatus
|
||||||
autoUpdaterResult: AutoUpdaterResult | null;
|
autoUpdaterResult: AutoUpdaterResult | null
|
||||||
isAutoUpdating: boolean;
|
isAutoUpdating: boolean
|
||||||
debug: boolean;
|
debug: boolean
|
||||||
verbose: boolean;
|
verbose: boolean
|
||||||
messages: Message[];
|
messages: Message[]
|
||||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
|
onAutoUpdaterResult: (result: AutoUpdaterResult) => void
|
||||||
onChangeIsUpdating: (isUpdating: boolean) => void;
|
onChangeIsUpdating: (isUpdating: boolean) => void
|
||||||
ideSelection: IDESelection | undefined;
|
ideSelection: IDESelection | undefined
|
||||||
mcpClients?: MCPServerConnection[];
|
mcpClients?: MCPServerConnection[]
|
||||||
isInputWrapped?: boolean;
|
isInputWrapped?: boolean
|
||||||
isNarrow?: boolean;
|
isNarrow?: boolean
|
||||||
};
|
}
|
||||||
export function Notifications(t0) {
|
|
||||||
const $ = _c(34);
|
export function Notifications({
|
||||||
const {
|
apiKeyStatus,
|
||||||
apiKeyStatus,
|
autoUpdaterResult,
|
||||||
autoUpdaterResult,
|
debug,
|
||||||
debug,
|
isAutoUpdating,
|
||||||
isAutoUpdating,
|
verbose,
|
||||||
verbose,
|
messages,
|
||||||
messages,
|
onAutoUpdaterResult,
|
||||||
onAutoUpdaterResult,
|
onChangeIsUpdating,
|
||||||
onChangeIsUpdating,
|
ideSelection,
|
||||||
ideSelection,
|
mcpClients,
|
||||||
mcpClients,
|
isInputWrapped = false,
|
||||||
isInputWrapped: t1,
|
isNarrow = false,
|
||||||
isNarrow: t2
|
}: Props): ReactNode {
|
||||||
} = t0;
|
const tokenUsage = useMemo(() => {
|
||||||
const isInputWrapped = t1 === undefined ? false : t1;
|
const messagesForTokenCount = getMessagesAfterCompactBoundary(messages)
|
||||||
const isNarrow = t2 === undefined ? false : t2;
|
return tokenCountFromLastAPIResponse(messagesForTokenCount)
|
||||||
let t3;
|
}, [messages])
|
||||||
if ($[0] !== messages) {
|
|
||||||
const messagesForTokenCount = getMessagesAfterCompactBoundary(messages);
|
// AppState-sourced model — same source as API requests. getMainLoopModel()
|
||||||
t3 = tokenCountFromLastAPIResponse(messagesForTokenCount);
|
// re-reads settings.json on every call, so another session's /model write
|
||||||
$[0] = messages;
|
// would leak into this session's display (anthropics/claude-code#37596).
|
||||||
$[1] = t3;
|
const mainLoopModel = useMainLoopModel()
|
||||||
} else {
|
const isShowingCompactMessage = calculateTokenWarningState(
|
||||||
t3 = $[1];
|
tokenUsage,
|
||||||
}
|
mainLoopModel,
|
||||||
const tokenUsage = t3;
|
).isAboveWarningThreshold
|
||||||
const mainLoopModel = useMainLoopModel();
|
const { status: ideStatus } = useIdeConnectionStatus(mcpClients)
|
||||||
let t4;
|
const notifications = useAppState(s => s.notifications)
|
||||||
if ($[2] !== mainLoopModel || $[3] !== tokenUsage) {
|
const { addNotification, removeNotification } = useNotifications()
|
||||||
t4 = calculateTokenWarningState(tokenUsage, mainLoopModel);
|
const claudeAiLimits = useClaudeAiLimits()
|
||||||
$[2] = mainLoopModel;
|
|
||||||
$[3] = tokenUsage;
|
// Register env hook notifier for CwdChanged/FileChanged feedback
|
||||||
$[4] = t4;
|
useEffect(() => {
|
||||||
} else {
|
setEnvHookNotifier((text, isError) => {
|
||||||
t4 = $[4];
|
addNotification({
|
||||||
}
|
key: 'env-hook',
|
||||||
const isShowingCompactMessage = t4.isAboveWarningThreshold;
|
text,
|
||||||
const {
|
color: isError ? 'error' : undefined,
|
||||||
status: ideStatus
|
priority: isError ? 'medium' : 'low',
|
||||||
} = useIdeConnectionStatus(mcpClients);
|
timeoutMs: isError ? 8000 : 5000,
|
||||||
const notifications = useAppState(_temp);
|
})
|
||||||
const {
|
})
|
||||||
|
return () => setEnvHookNotifier(null)
|
||||||
|
}, [addNotification])
|
||||||
|
|
||||||
|
// Check if we should show the IDE selection indicator
|
||||||
|
const shouldShowIdeSelection =
|
||||||
|
ideStatus === 'connected' &&
|
||||||
|
(ideSelection?.filePath ||
|
||||||
|
(ideSelection?.text && ideSelection.lineCount > 0))
|
||||||
|
|
||||||
|
// Hide update installed message when showing IDE selection
|
||||||
|
const shouldShowAutoUpdater =
|
||||||
|
!shouldShowIdeSelection ||
|
||||||
|
isAutoUpdating ||
|
||||||
|
autoUpdaterResult?.status !== 'success'
|
||||||
|
|
||||||
|
// Check if we're in overage mode for UI indicators
|
||||||
|
const isInOverageMode = claudeAiLimits.isUsingOverage
|
||||||
|
const subscriptionType = getSubscriptionType()
|
||||||
|
const isTeamOrEnterprise =
|
||||||
|
subscriptionType === 'team' || subscriptionType === 'enterprise'
|
||||||
|
|
||||||
|
// Check if the external editor hint should be shown
|
||||||
|
const editor = getExternalEditor()
|
||||||
|
const shouldShowExternalEditorHint =
|
||||||
|
isInputWrapped &&
|
||||||
|
!isShowingCompactMessage &&
|
||||||
|
apiKeyStatus !== 'invalid' &&
|
||||||
|
apiKeyStatus !== 'missing' &&
|
||||||
|
editor !== undefined
|
||||||
|
|
||||||
|
// Show external editor hint as notification when input is wrapped
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldShowExternalEditorHint && editor) {
|
||||||
|
logEvent('tengu_external_editor_hint_shown', {})
|
||||||
|
addNotification({
|
||||||
|
key: 'external-editor-hint',
|
||||||
|
jsx: (
|
||||||
|
<Text dimColor>
|
||||||
|
<ConfigurableShortcutHint
|
||||||
|
action="chat:externalEditor"
|
||||||
|
context="Chat"
|
||||||
|
fallback="ctrl+g"
|
||||||
|
description={`edit in ${toIDEDisplayName(editor)}`}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
priority: 'immediate',
|
||||||
|
timeoutMs: 5000,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
removeNotification('external-editor-hint')
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
shouldShowExternalEditorHint,
|
||||||
|
editor,
|
||||||
addNotification,
|
addNotification,
|
||||||
removeNotification
|
removeNotification,
|
||||||
} = useNotifications();
|
])
|
||||||
const claudeAiLimits = useClaudeAiLimits();
|
|
||||||
let t5;
|
return (
|
||||||
let t6;
|
<SentryErrorBoundary>
|
||||||
if ($[5] !== addNotification) {
|
<Box
|
||||||
t5 = () => {
|
flexDirection="column"
|
||||||
setEnvHookNotifier((text, isError) => {
|
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
||||||
addNotification({
|
flexShrink={0}
|
||||||
key: "env-hook",
|
overflowX="hidden"
|
||||||
text,
|
>
|
||||||
color: isError ? "error" : undefined,
|
<NotificationContent
|
||||||
priority: isError ? "medium" : "low",
|
ideSelection={ideSelection}
|
||||||
timeoutMs: isError ? 8000 : 5000
|
mcpClients={mcpClients}
|
||||||
});
|
notifications={notifications}
|
||||||
});
|
isInOverageMode={isInOverageMode ?? false}
|
||||||
return _temp2;
|
isTeamOrEnterprise={isTeamOrEnterprise}
|
||||||
};
|
apiKeyStatus={apiKeyStatus}
|
||||||
t6 = [addNotification];
|
debug={debug}
|
||||||
$[5] = addNotification;
|
verbose={verbose}
|
||||||
$[6] = t5;
|
tokenUsage={tokenUsage}
|
||||||
$[7] = t6;
|
mainLoopModel={mainLoopModel}
|
||||||
} else {
|
shouldShowAutoUpdater={shouldShowAutoUpdater}
|
||||||
t5 = $[6];
|
autoUpdaterResult={autoUpdaterResult}
|
||||||
t6 = $[7];
|
isAutoUpdating={isAutoUpdating}
|
||||||
}
|
isShowingCompactMessage={isShowingCompactMessage}
|
||||||
useEffect(t5, t6);
|
onAutoUpdaterResult={onAutoUpdaterResult}
|
||||||
const shouldShowIdeSelection = ideStatus === "connected" && (ideSelection?.filePath || ideSelection?.text && ideSelection.lineCount > 0);
|
onChangeIsUpdating={onChangeIsUpdating}
|
||||||
const shouldShowAutoUpdater = !shouldShowIdeSelection || isAutoUpdating || autoUpdaterResult?.status !== "success";
|
/>
|
||||||
const isInOverageMode = claudeAiLimits.isUsingOverage;
|
</Box>
|
||||||
let t7;
|
</SentryErrorBoundary>
|
||||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
)
|
||||||
t7 = getSubscriptionType();
|
|
||||||
$[8] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[8];
|
|
||||||
}
|
|
||||||
const subscriptionType = t7;
|
|
||||||
const isTeamOrEnterprise = subscriptionType === "team" || subscriptionType === "enterprise";
|
|
||||||
let t8;
|
|
||||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t8 = getExternalEditor();
|
|
||||||
$[9] = t8;
|
|
||||||
} else {
|
|
||||||
t8 = $[9];
|
|
||||||
}
|
|
||||||
const editor = t8;
|
|
||||||
const shouldShowExternalEditorHint = isInputWrapped && !isShowingCompactMessage && apiKeyStatus !== "invalid" && apiKeyStatus !== "missing" && editor !== undefined;
|
|
||||||
let t10;
|
|
||||||
let t9;
|
|
||||||
if ($[10] !== addNotification || $[11] !== removeNotification || $[12] !== shouldShowExternalEditorHint) {
|
|
||||||
t9 = () => {
|
|
||||||
if (shouldShowExternalEditorHint && editor) {
|
|
||||||
logEvent("tengu_external_editor_hint_shown", {});
|
|
||||||
addNotification({
|
|
||||||
key: "external-editor-hint",
|
|
||||||
jsx: <Text dimColor={true}><ConfigurableShortcutHint action="chat:externalEditor" context="Chat" fallback="ctrl+g" description={`edit in ${toIDEDisplayName(editor)}`} /></Text>,
|
|
||||||
priority: "immediate",
|
|
||||||
timeoutMs: 5000
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
removeNotification("external-editor-hint");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
t10 = [shouldShowExternalEditorHint, editor, addNotification, removeNotification];
|
|
||||||
$[10] = addNotification;
|
|
||||||
$[11] = removeNotification;
|
|
||||||
$[12] = shouldShowExternalEditorHint;
|
|
||||||
$[13] = t10;
|
|
||||||
$[14] = t9;
|
|
||||||
} else {
|
|
||||||
t10 = $[13];
|
|
||||||
t9 = $[14];
|
|
||||||
}
|
|
||||||
useEffect(t9, t10);
|
|
||||||
const t11 = isNarrow ? "flex-start" : "flex-end";
|
|
||||||
const t12 = isInOverageMode ?? false;
|
|
||||||
let t13;
|
|
||||||
if ($[15] !== apiKeyStatus || $[16] !== autoUpdaterResult || $[17] !== debug || $[18] !== ideSelection || $[19] !== isAutoUpdating || $[20] !== isShowingCompactMessage || $[21] !== mainLoopModel || $[22] !== mcpClients || $[23] !== notifications || $[24] !== onAutoUpdaterResult || $[25] !== onChangeIsUpdating || $[26] !== shouldShowAutoUpdater || $[27] !== t12 || $[28] !== tokenUsage || $[29] !== verbose) {
|
|
||||||
t13 = <NotificationContent ideSelection={ideSelection} mcpClients={mcpClients} notifications={notifications} isInOverageMode={t12} isTeamOrEnterprise={isTeamOrEnterprise} apiKeyStatus={apiKeyStatus} debug={debug} verbose={verbose} tokenUsage={tokenUsage} mainLoopModel={mainLoopModel} shouldShowAutoUpdater={shouldShowAutoUpdater} autoUpdaterResult={autoUpdaterResult} isAutoUpdating={isAutoUpdating} isShowingCompactMessage={isShowingCompactMessage} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={onChangeIsUpdating} />;
|
|
||||||
$[15] = apiKeyStatus;
|
|
||||||
$[16] = autoUpdaterResult;
|
|
||||||
$[17] = debug;
|
|
||||||
$[18] = ideSelection;
|
|
||||||
$[19] = isAutoUpdating;
|
|
||||||
$[20] = isShowingCompactMessage;
|
|
||||||
$[21] = mainLoopModel;
|
|
||||||
$[22] = mcpClients;
|
|
||||||
$[23] = notifications;
|
|
||||||
$[24] = onAutoUpdaterResult;
|
|
||||||
$[25] = onChangeIsUpdating;
|
|
||||||
$[26] = shouldShowAutoUpdater;
|
|
||||||
$[27] = t12;
|
|
||||||
$[28] = tokenUsage;
|
|
||||||
$[29] = verbose;
|
|
||||||
$[30] = t13;
|
|
||||||
} else {
|
|
||||||
t13 = $[30];
|
|
||||||
}
|
|
||||||
let t14;
|
|
||||||
if ($[31] !== t11 || $[32] !== t13) {
|
|
||||||
t14 = <SentryErrorBoundary><Box flexDirection="column" alignItems={t11} flexShrink={0} overflowX="hidden">{t13}</Box></SentryErrorBoundary>;
|
|
||||||
$[31] = t11;
|
|
||||||
$[32] = t13;
|
|
||||||
$[33] = t14;
|
|
||||||
} else {
|
|
||||||
t14 = $[33];
|
|
||||||
}
|
|
||||||
return t14;
|
|
||||||
}
|
|
||||||
function _temp2() {
|
|
||||||
return setEnvHookNotifier(null);
|
|
||||||
}
|
|
||||||
function _temp(s) {
|
|
||||||
return s.notifications;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function NotificationContent({
|
function NotificationContent({
|
||||||
ideSelection,
|
ideSelection,
|
||||||
mcpClients,
|
mcpClients,
|
||||||
@@ -229,103 +212,155 @@ function NotificationContent({
|
|||||||
isAutoUpdating,
|
isAutoUpdating,
|
||||||
isShowingCompactMessage,
|
isShowingCompactMessage,
|
||||||
onAutoUpdaterResult,
|
onAutoUpdaterResult,
|
||||||
onChangeIsUpdating
|
onChangeIsUpdating,
|
||||||
}: {
|
}: {
|
||||||
ideSelection: IDESelection | undefined;
|
ideSelection: IDESelection | undefined
|
||||||
mcpClients?: MCPServerConnection[];
|
mcpClients?: MCPServerConnection[]
|
||||||
notifications: {
|
notifications: {
|
||||||
current: Notification | null;
|
current: Notification | null
|
||||||
queue: Notification[];
|
queue: Notification[]
|
||||||
};
|
}
|
||||||
isInOverageMode: boolean;
|
isInOverageMode: boolean
|
||||||
isTeamOrEnterprise: boolean;
|
isTeamOrEnterprise: boolean
|
||||||
apiKeyStatus: VerificationStatus;
|
apiKeyStatus: VerificationStatus
|
||||||
debug: boolean;
|
debug: boolean
|
||||||
verbose: boolean;
|
verbose: boolean
|
||||||
tokenUsage: number;
|
tokenUsage: number
|
||||||
mainLoopModel: string;
|
mainLoopModel: string
|
||||||
shouldShowAutoUpdater: boolean;
|
shouldShowAutoUpdater: boolean
|
||||||
autoUpdaterResult: AutoUpdaterResult | null;
|
autoUpdaterResult: AutoUpdaterResult | null
|
||||||
isAutoUpdating: boolean;
|
isAutoUpdating: boolean
|
||||||
isShowingCompactMessage: boolean;
|
isShowingCompactMessage: boolean
|
||||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
|
onAutoUpdaterResult: (result: AutoUpdaterResult) => void
|
||||||
onChangeIsUpdating: (isUpdating: boolean) => void;
|
onChangeIsUpdating: (isUpdating: boolean) => void
|
||||||
}): ReactNode {
|
}): ReactNode {
|
||||||
// Poll apiKeyHelper inflight state to show slow-helper notice.
|
// Poll apiKeyHelper inflight state to show slow-helper notice.
|
||||||
// Gated on configuration — most users never set apiKeyHelper, so the
|
// Gated on configuration — most users never set apiKeyHelper, so the
|
||||||
// effect is a no-op for them (no interval allocated).
|
// effect is a no-op for them (no interval allocated).
|
||||||
const [apiKeyHelperSlow, setApiKeyHelperSlow] = useState<string | null>(null);
|
const [apiKeyHelperSlow, setApiKeyHelperSlow] = useState<string | null>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!getConfiguredApiKeyHelper()) return;
|
if (!getConfiguredApiKeyHelper()) return
|
||||||
const interval = setInterval((setSlow: React.Dispatch<React.SetStateAction<string | null>>) => {
|
const interval = setInterval(
|
||||||
const ms = getApiKeyHelperElapsedMs();
|
(setSlow: React.Dispatch<React.SetStateAction<string | null>>) => {
|
||||||
const next = ms >= 10_000 ? formatDuration(ms) : null;
|
const ms = getApiKeyHelperElapsedMs()
|
||||||
setSlow(prev => next === prev ? prev : next);
|
const next = ms >= 10_000 ? formatDuration(ms) : null
|
||||||
}, 1000, setApiKeyHelperSlow);
|
setSlow(prev => (next === prev ? prev : next))
|
||||||
return () => clearInterval(interval);
|
},
|
||||||
}, []);
|
1000,
|
||||||
|
setApiKeyHelperSlow,
|
||||||
|
)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Voice state (VOICE_MODE builds only, runtime-gated by GrowthBook)
|
// Voice state (VOICE_MODE builds only, runtime-gated by GrowthBook)
|
||||||
const voiceState = feature('VOICE_MODE') ?
|
const voiceState = feature('VOICE_MODE')
|
||||||
|
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||||
|
useVoiceState(s => s.voiceState)
|
||||||
|
: ('idle' as const)
|
||||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||||
useVoiceState(s => s.voiceState) : 'idle' as const;
|
const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false
|
||||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
const voiceError = feature('VOICE_MODE')
|
||||||
const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false;
|
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||||
const voiceError = feature('VOICE_MODE') ?
|
useVoiceState(s => s.voiceError)
|
||||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
: null
|
||||||
useVoiceState(s_0 => s_0.voiceError) : null;
|
const isBriefOnly =
|
||||||
const isBriefOnly = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||||
useAppState(s_1 => s_1.isBriefOnly) : false;
|
useAppState(s => s.isBriefOnly)
|
||||||
|
: false
|
||||||
|
|
||||||
// When voice is actively recording or processing, replace all
|
// When voice is actively recording or processing, replace all
|
||||||
// notifications with just the voice indicator.
|
// notifications with just the voice indicator.
|
||||||
if (feature('VOICE_MODE') && voiceEnabled && (voiceState === 'recording' || voiceState === 'processing')) {
|
if (
|
||||||
return <VoiceIndicator voiceState={voiceState} />;
|
feature('VOICE_MODE') &&
|
||||||
|
voiceEnabled &&
|
||||||
|
(voiceState === 'recording' || voiceState === 'processing')
|
||||||
|
) {
|
||||||
|
return <VoiceIndicator voiceState={voiceState} />
|
||||||
}
|
}
|
||||||
return <>
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<IdeStatusIndicator ideSelection={ideSelection} mcpClients={mcpClients} />
|
<IdeStatusIndicator ideSelection={ideSelection} mcpClients={mcpClients} />
|
||||||
{notifications.current && ('jsx' in notifications.current ? <Text wrap="truncate" key={notifications.current.key}>
|
{notifications.current &&
|
||||||
|
('jsx' in notifications.current ? (
|
||||||
|
<Text wrap="truncate" key={notifications.current.key}>
|
||||||
{notifications.current.jsx}
|
{notifications.current.jsx}
|
||||||
</Text> : <Text color={notifications.current.color} dimColor={!notifications.current.color} wrap="truncate">
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
color={notifications.current.color}
|
||||||
|
dimColor={!notifications.current.color}
|
||||||
|
wrap="truncate"
|
||||||
|
>
|
||||||
{notifications.current.text}
|
{notifications.current.text}
|
||||||
</Text>)}
|
</Text>
|
||||||
{isInOverageMode && !isTeamOrEnterprise && <Box>
|
))}
|
||||||
|
{isInOverageMode && !isTeamOrEnterprise && (
|
||||||
|
<Box>
|
||||||
<Text dimColor wrap="truncate">
|
<Text dimColor wrap="truncate">
|
||||||
Now using extra usage
|
Now using extra usage
|
||||||
</Text>
|
</Text>
|
||||||
</Box>}
|
</Box>
|
||||||
{apiKeyHelperSlow && <Box>
|
)}
|
||||||
|
{apiKeyHelperSlow && (
|
||||||
|
<Box>
|
||||||
<Text color="warning" wrap="truncate">
|
<Text color="warning" wrap="truncate">
|
||||||
apiKeyHelper is taking a while{' '}
|
apiKeyHelper is taking a while{' '}
|
||||||
</Text>
|
</Text>
|
||||||
<Text dimColor wrap="truncate">
|
<Text dimColor wrap="truncate">
|
||||||
({apiKeyHelperSlow})
|
({apiKeyHelperSlow})
|
||||||
</Text>
|
</Text>
|
||||||
</Box>}
|
</Box>
|
||||||
{(apiKeyStatus === 'invalid' || apiKeyStatus === 'missing') && <Box>
|
)}
|
||||||
|
{(apiKeyStatus === 'invalid' || apiKeyStatus === 'missing') && (
|
||||||
|
<Box>
|
||||||
<Text color="error" wrap="truncate">
|
<Text color="error" wrap="truncate">
|
||||||
{isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ? 'Authentication error · Try again' : 'Not logged in · Run /login'}
|
{isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)
|
||||||
|
? 'Authentication error · Try again'
|
||||||
|
: 'Not logged in · Run /login'}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>}
|
</Box>
|
||||||
{debug && <Box>
|
)}
|
||||||
|
{debug && (
|
||||||
|
<Box>
|
||||||
<Text color="warning" wrap="truncate">
|
<Text color="warning" wrap="truncate">
|
||||||
Debug mode
|
Debug mode
|
||||||
</Text>
|
</Text>
|
||||||
</Box>}
|
</Box>
|
||||||
{apiKeyStatus !== 'invalid' && apiKeyStatus !== 'missing' && verbose && <Box>
|
)}
|
||||||
|
{apiKeyStatus !== 'invalid' && apiKeyStatus !== 'missing' && verbose && (
|
||||||
|
<Box>
|
||||||
<Text dimColor wrap="truncate">
|
<Text dimColor wrap="truncate">
|
||||||
{tokenUsage} tokens
|
{tokenUsage} tokens
|
||||||
</Text>
|
</Text>
|
||||||
</Box>}
|
</Box>
|
||||||
{!isBriefOnly && <TokenWarning tokenUsage={tokenUsage} model={mainLoopModel} />}
|
)}
|
||||||
{shouldShowAutoUpdater && <AutoUpdaterWrapper verbose={verbose} onAutoUpdaterResult={onAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} isUpdating={isAutoUpdating} onChangeIsUpdating={onChangeIsUpdating} showSuccessMessage={!isShowingCompactMessage} />}
|
{!isBriefOnly && (
|
||||||
{feature('VOICE_MODE') ? voiceEnabled && voiceError && <Box>
|
<TokenWarning tokenUsage={tokenUsage} model={mainLoopModel} />
|
||||||
|
)}
|
||||||
|
{shouldShowAutoUpdater && (
|
||||||
|
<AutoUpdaterWrapper
|
||||||
|
verbose={verbose}
|
||||||
|
onAutoUpdaterResult={onAutoUpdaterResult}
|
||||||
|
autoUpdaterResult={autoUpdaterResult}
|
||||||
|
isUpdating={isAutoUpdating}
|
||||||
|
onChangeIsUpdating={onChangeIsUpdating}
|
||||||
|
showSuccessMessage={!isShowingCompactMessage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{feature('VOICE_MODE')
|
||||||
|
? voiceEnabled &&
|
||||||
|
voiceError && (
|
||||||
|
<Box>
|
||||||
<Text color="error" wrap="truncate">
|
<Text color="error" wrap="truncate">
|
||||||
{voiceError}
|
{voiceError}
|
||||||
</Text>
|
</Text>
|
||||||
</Box> : null}
|
</Box>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
<MemoryUsageIndicator />
|
<MemoryUsageIndicator />
|
||||||
<SandboxPromptFooterHint />
|
<SandboxPromptFooterHint />
|
||||||
</>;
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +1,77 @@
|
|||||||
import { feature } from 'bun:bundle';
|
import { feature } from 'bun:bundle'
|
||||||
import * as React from 'react';
|
import * as React from 'react'
|
||||||
import { memo, type ReactNode, useMemo, useRef } from 'react';
|
import { memo, type ReactNode, useMemo, useRef } from 'react'
|
||||||
import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js';
|
import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js'
|
||||||
import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js';
|
import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js'
|
||||||
import { useSetPromptOverlay } from '../../context/promptOverlayContext.js';
|
import { useSetPromptOverlay } from '../../context/promptOverlayContext.js'
|
||||||
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js';
|
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
|
||||||
import type { IDESelection } from '../../hooks/useIdeSelection.js';
|
import type { IDESelection } from '../../hooks/useIdeSelection.js'
|
||||||
import { useSettings } from '../../hooks/useSettings.js';
|
import { useSettings } from '../../hooks/useSettings.js'
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import type { MCPServerConnection } from '../../services/mcp/types.js';
|
import type { MCPServerConnection } from '../../services/mcp/types.js'
|
||||||
import { useAppState } from '../../state/AppState.js';
|
import { useAppState } from '../../state/AppState.js'
|
||||||
import type { ToolPermissionContext } from '../../Tool.js';
|
import type { ToolPermissionContext } from '../../Tool.js'
|
||||||
import type { Message } from '../../types/message.js';
|
import type { Message } from '../../types/message.js'
|
||||||
import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js';
|
import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js'
|
||||||
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js';
|
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
|
||||||
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js';
|
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
|
||||||
import { isUndercover } from '../../utils/undercover.js';
|
import { isUndercover } from '../../utils/undercover.js'
|
||||||
import { CoordinatorTaskPanel, useCoordinatorTaskCount } from '../CoordinatorAgentStatus.js';
|
import {
|
||||||
import { getLastAssistantMessageId, StatusLine, statusLineShouldDisplay } from '../StatusLine.js';
|
CoordinatorTaskPanel,
|
||||||
import { Notifications } from './Notifications.js';
|
useCoordinatorTaskCount,
|
||||||
import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js';
|
} from '../CoordinatorAgentStatus.js'
|
||||||
import { PromptInputFooterSuggestions, type SuggestionItem } from './PromptInputFooterSuggestions.js';
|
import {
|
||||||
import { PromptInputHelpMenu } from './PromptInputHelpMenu.js';
|
getLastAssistantMessageId,
|
||||||
|
StatusLine,
|
||||||
|
statusLineShouldDisplay,
|
||||||
|
} from '../StatusLine.js'
|
||||||
|
import { Notifications } from './Notifications.js'
|
||||||
|
import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js'
|
||||||
|
import {
|
||||||
|
PromptInputFooterSuggestions,
|
||||||
|
type SuggestionItem,
|
||||||
|
} from './PromptInputFooterSuggestions.js'
|
||||||
|
import { PromptInputHelpMenu } from './PromptInputHelpMenu.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
apiKeyStatus: VerificationStatus;
|
apiKeyStatus: VerificationStatus
|
||||||
debug: boolean;
|
debug: boolean
|
||||||
exitMessage: {
|
exitMessage: {
|
||||||
show: boolean;
|
show: boolean
|
||||||
key?: string;
|
key?: string
|
||||||
};
|
}
|
||||||
vimMode: VimMode | undefined;
|
vimMode: VimMode | undefined
|
||||||
mode: PromptInputMode;
|
mode: PromptInputMode
|
||||||
autoUpdaterResult: AutoUpdaterResult | null;
|
autoUpdaterResult: AutoUpdaterResult | null
|
||||||
isAutoUpdating: boolean;
|
isAutoUpdating: boolean
|
||||||
verbose: boolean;
|
verbose: boolean
|
||||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
|
onAutoUpdaterResult: (result: AutoUpdaterResult) => void
|
||||||
onChangeIsUpdating: (isUpdating: boolean) => void;
|
onChangeIsUpdating: (isUpdating: boolean) => void
|
||||||
suggestions: SuggestionItem[];
|
suggestions: SuggestionItem[]
|
||||||
selectedSuggestion: number;
|
selectedSuggestion: number
|
||||||
maxColumnWidth?: number;
|
maxColumnWidth?: number
|
||||||
toolPermissionContext: ToolPermissionContext;
|
toolPermissionContext: ToolPermissionContext
|
||||||
helpOpen: boolean;
|
helpOpen: boolean
|
||||||
suppressHint: boolean;
|
suppressHint: boolean
|
||||||
isLoading: boolean;
|
isLoading: boolean
|
||||||
tasksSelected: boolean;
|
tasksSelected: boolean
|
||||||
teamsSelected: boolean;
|
teamsSelected: boolean
|
||||||
bridgeSelected: boolean;
|
bridgeSelected: boolean
|
||||||
tmuxSelected: boolean;
|
tmuxSelected: boolean
|
||||||
teammateFooterIndex?: number;
|
teammateFooterIndex?: number
|
||||||
ideSelection: IDESelection | undefined;
|
ideSelection: IDESelection | undefined
|
||||||
mcpClients?: MCPServerConnection[];
|
mcpClients?: MCPServerConnection[]
|
||||||
isPasting?: boolean;
|
isPasting?: boolean
|
||||||
isInputWrapped?: boolean;
|
isInputWrapped?: boolean
|
||||||
messages: Message[];
|
messages: Message[]
|
||||||
isSearching: boolean;
|
isSearching: boolean
|
||||||
historyQuery: string;
|
historyQuery: string
|
||||||
setHistoryQuery: (query: string) => void;
|
setHistoryQuery: (query: string) => void
|
||||||
historyFailedMatch: boolean;
|
historyFailedMatch: boolean
|
||||||
onOpenTasksDialog?: (taskId?: string) => void;
|
onOpenTasksDialog?: (taskId?: string) => void
|
||||||
};
|
}
|
||||||
|
|
||||||
function PromptInputFooter({
|
function PromptInputFooter({
|
||||||
apiKeyStatus,
|
apiKeyStatus,
|
||||||
debug,
|
debug,
|
||||||
@@ -92,99 +104,176 @@ function PromptInputFooter({
|
|||||||
historyQuery,
|
historyQuery,
|
||||||
setHistoryQuery,
|
setHistoryQuery,
|
||||||
historyFailedMatch,
|
historyFailedMatch,
|
||||||
onOpenTasksDialog
|
onOpenTasksDialog,
|
||||||
}: Props): ReactNode {
|
}: Props): ReactNode {
|
||||||
const settings = useSettings();
|
const settings = useSettings()
|
||||||
const {
|
const { columns, rows } = useTerminalSize()
|
||||||
columns,
|
const messagesRef = useRef(messages)
|
||||||
rows
|
messagesRef.current = messages
|
||||||
} = useTerminalSize();
|
const lastAssistantMessageId = useMemo(
|
||||||
const messagesRef = useRef(messages);
|
() => getLastAssistantMessageId(messages),
|
||||||
messagesRef.current = messages;
|
[messages],
|
||||||
const lastAssistantMessageId = useMemo(() => getLastAssistantMessageId(messages), [messages]);
|
)
|
||||||
const isNarrow = columns < 80;
|
const isNarrow = columns < 80
|
||||||
// In fullscreen the bottom slot is flexShrink:0, so every row here is a row
|
// In fullscreen the bottom slot is flexShrink:0, so every row here is a row
|
||||||
// stolen from the ScrollBox. Drop the optional StatusLine first. Non-fullscreen
|
// stolen from the ScrollBox. Drop the optional StatusLine first. Non-fullscreen
|
||||||
// has terminal scrollback to absorb overflow, so we never hide StatusLine there.
|
// has terminal scrollback to absorb overflow, so we never hide StatusLine there.
|
||||||
const isFullscreen = isFullscreenEnvEnabled();
|
const isFullscreen = isFullscreenEnvEnabled()
|
||||||
const isShort = isFullscreen && rows < 24;
|
const isShort = isFullscreen && rows < 24
|
||||||
|
|
||||||
// Pill highlights when tasks is the active footer item AND no specific
|
// Pill highlights when tasks is the active footer item AND no specific
|
||||||
// agent row is selected. When coordinatorTaskIndex >= 0 the pointer has
|
// agent row is selected. When coordinatorTaskIndex >= 0 the pointer has
|
||||||
// moved into CoordinatorTaskPanel, so the pill should un-highlight.
|
// moved into CoordinatorTaskPanel, so the pill should un-highlight.
|
||||||
// coordinatorTaskCount === 0 covers the bash-only case (no agent rows
|
// coordinatorTaskCount === 0 covers the bash-only case (no agent rows
|
||||||
// exist, pill is the only selectable item).
|
// exist, pill is the only selectable item).
|
||||||
const coordinatorTaskCount = useCoordinatorTaskCount();
|
const coordinatorTaskCount = useCoordinatorTaskCount()
|
||||||
const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex);
|
const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex)
|
||||||
const pillSelected = tasksSelected && (coordinatorTaskCount === 0 || coordinatorTaskIndex < 0);
|
const pillSelected =
|
||||||
|
tasksSelected && (coordinatorTaskCount === 0 || coordinatorTaskIndex < 0)
|
||||||
|
|
||||||
// Hide `? for shortcuts` if the user has a custom status line, or during ctrl-r
|
// Hide `? for shortcuts` if the user has a custom status line, or during ctrl-r
|
||||||
const suppressHint = suppressHintFromProps || statusLineShouldDisplay(settings) || isSearching;
|
const suppressHint =
|
||||||
|
suppressHintFromProps || statusLineShouldDisplay(settings) || isSearching
|
||||||
// Fullscreen: portal data to FullscreenLayout — see promptOverlayContext.tsx
|
// Fullscreen: portal data to FullscreenLayout — see promptOverlayContext.tsx
|
||||||
const overlayData = useMemo(() => isFullscreen && suggestions.length ? {
|
const overlayData = useMemo(
|
||||||
suggestions,
|
() =>
|
||||||
selectedSuggestion,
|
isFullscreen && suggestions.length
|
||||||
maxColumnWidth
|
? { suggestions, selectedSuggestion, maxColumnWidth }
|
||||||
} : null, [isFullscreen, suggestions, selectedSuggestion, maxColumnWidth]);
|
: null,
|
||||||
useSetPromptOverlay(overlayData);
|
[isFullscreen, suggestions, selectedSuggestion, maxColumnWidth],
|
||||||
|
)
|
||||||
|
useSetPromptOverlay(overlayData)
|
||||||
|
|
||||||
if (suggestions.length && !isFullscreen) {
|
if (suggestions.length && !isFullscreen) {
|
||||||
return <Box paddingX={2} paddingY={0}>
|
return (
|
||||||
<PromptInputFooterSuggestions suggestions={suggestions} selectedSuggestion={selectedSuggestion} maxColumnWidth={maxColumnWidth} />
|
<Box paddingX={2} paddingY={0}>
|
||||||
</Box>;
|
<PromptInputFooterSuggestions
|
||||||
|
suggestions={suggestions}
|
||||||
|
selectedSuggestion={selectedSuggestion}
|
||||||
|
maxColumnWidth={maxColumnWidth}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (helpOpen) {
|
if (helpOpen) {
|
||||||
return <PromptInputHelpMenu dimColor={true} fixedWidth={true} paddingX={2} />;
|
return (
|
||||||
|
<PromptInputHelpMenu dimColor={true} fixedWidth={true} paddingX={2} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return <>
|
|
||||||
<Box flexDirection={isNarrow ? 'column' : 'row'} justifyContent={isNarrow ? 'flex-start' : 'space-between'} paddingX={2} gap={isNarrow ? 0 : 1}>
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
flexDirection={isNarrow ? 'column' : 'row'}
|
||||||
|
justifyContent={isNarrow ? 'flex-start' : 'space-between'}
|
||||||
|
paddingX={2}
|
||||||
|
gap={isNarrow ? 0 : 1}
|
||||||
|
>
|
||||||
<Box flexDirection="column" flexShrink={isNarrow ? 0 : 1}>
|
<Box flexDirection="column" flexShrink={isNarrow ? 0 : 1}>
|
||||||
{mode === 'prompt' && !isShort && !exitMessage.show && !isPasting && statusLineShouldDisplay(settings) && <StatusLine messagesRef={messagesRef} lastAssistantMessageId={lastAssistantMessageId} vimMode={vimMode} />}
|
{mode === 'prompt' &&
|
||||||
<PromptInputFooterLeftSide exitMessage={exitMessage} vimMode={vimMode} mode={mode} toolPermissionContext={toolPermissionContext} suppressHint={suppressHint} isLoading={isLoading} tasksSelected={pillSelected} teamsSelected={teamsSelected} teammateFooterIndex={teammateFooterIndex} tmuxSelected={tmuxSelected} isPasting={isPasting} isSearching={isSearching} historyQuery={historyQuery} setHistoryQuery={setHistoryQuery} historyFailedMatch={historyFailedMatch} onOpenTasksDialog={onOpenTasksDialog} />
|
!isShort &&
|
||||||
|
!exitMessage.show &&
|
||||||
|
!isPasting &&
|
||||||
|
statusLineShouldDisplay(settings) && (
|
||||||
|
<StatusLine
|
||||||
|
messagesRef={messagesRef}
|
||||||
|
lastAssistantMessageId={lastAssistantMessageId}
|
||||||
|
vimMode={vimMode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<PromptInputFooterLeftSide
|
||||||
|
exitMessage={exitMessage}
|
||||||
|
vimMode={vimMode}
|
||||||
|
mode={mode}
|
||||||
|
toolPermissionContext={toolPermissionContext}
|
||||||
|
suppressHint={suppressHint}
|
||||||
|
isLoading={isLoading}
|
||||||
|
tasksSelected={pillSelected}
|
||||||
|
teamsSelected={teamsSelected}
|
||||||
|
teammateFooterIndex={teammateFooterIndex}
|
||||||
|
tmuxSelected={tmuxSelected}
|
||||||
|
isPasting={isPasting}
|
||||||
|
isSearching={isSearching}
|
||||||
|
historyQuery={historyQuery}
|
||||||
|
setHistoryQuery={setHistoryQuery}
|
||||||
|
historyFailedMatch={historyFailedMatch}
|
||||||
|
onOpenTasksDialog={onOpenTasksDialog}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexShrink={1} gap={1}>
|
<Box flexShrink={1} gap={1}>
|
||||||
{isFullscreen ? null : <Notifications apiKeyStatus={apiKeyStatus} autoUpdaterResult={autoUpdaterResult} debug={debug} isAutoUpdating={isAutoUpdating} verbose={verbose} messages={messages} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={onChangeIsUpdating} ideSelection={ideSelection} mcpClients={mcpClients} isInputWrapped={isInputWrapped} isNarrow={isNarrow} />}
|
{isFullscreen ? null : (
|
||||||
{(process.env.USER_TYPE) === 'ant' && isUndercover() && <Text dimColor>undercover</Text>}
|
<Notifications
|
||||||
|
apiKeyStatus={apiKeyStatus}
|
||||||
|
autoUpdaterResult={autoUpdaterResult}
|
||||||
|
debug={debug}
|
||||||
|
isAutoUpdating={isAutoUpdating}
|
||||||
|
verbose={verbose}
|
||||||
|
messages={messages}
|
||||||
|
onAutoUpdaterResult={onAutoUpdaterResult}
|
||||||
|
onChangeIsUpdating={onChangeIsUpdating}
|
||||||
|
ideSelection={ideSelection}
|
||||||
|
mcpClients={mcpClients}
|
||||||
|
isInputWrapped={isInputWrapped}
|
||||||
|
isNarrow={isNarrow}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{process.env.USER_TYPE === 'ant' && isUndercover() && (
|
||||||
|
<Text dimColor>undercover</Text>
|
||||||
|
)}
|
||||||
<BridgeStatusIndicator bridgeSelected={bridgeSelected} />
|
<BridgeStatusIndicator bridgeSelected={bridgeSelected} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{(process.env.USER_TYPE) === 'ant' && <CoordinatorTaskPanel />}
|
{process.env.USER_TYPE === 'ant' && <CoordinatorTaskPanel />}
|
||||||
</>;
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
export default memo(PromptInputFooter);
|
|
||||||
|
export default memo(PromptInputFooter)
|
||||||
|
|
||||||
type BridgeStatusProps = {
|
type BridgeStatusProps = {
|
||||||
bridgeSelected: boolean;
|
bridgeSelected: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
function BridgeStatusIndicator({
|
function BridgeStatusIndicator({
|
||||||
bridgeSelected
|
bridgeSelected,
|
||||||
}: BridgeStatusProps): React.ReactNode {
|
}: BridgeStatusProps): React.ReactNode {
|
||||||
if (!feature('BRIDGE_MODE')) return null;
|
if (!feature('BRIDGE_MODE')) return null
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||||
const enabled = useAppState(s => s.replBridgeEnabled);
|
const enabled = useAppState(s => s.replBridgeEnabled)
|
||||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||||
const connected = useAppState(s_0 => s_0.replBridgeConnected);
|
const connected = useAppState(s => s.replBridgeConnected)
|
||||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||||
const sessionActive = useAppState(s_1 => s_1.replBridgeSessionActive);
|
const sessionActive = useAppState(s => s.replBridgeSessionActive)
|
||||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||||
const reconnecting = useAppState(s_2 => s_2.replBridgeReconnecting);
|
const reconnecting = useAppState(s => s.replBridgeReconnecting)
|
||||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||||
const explicit = useAppState(s_3 => s_3.replBridgeExplicit);
|
const explicit = useAppState(s => s.replBridgeExplicit)
|
||||||
|
|
||||||
// Failed state is surfaced via notification (useReplBridge), not a footer pill.
|
// Failed state is surfaced via notification (useReplBridge), not a footer pill.
|
||||||
if (!isBridgeEnabled() || !enabled) return null;
|
if (!isBridgeEnabled() || !enabled) return null
|
||||||
|
|
||||||
const status = getBridgeStatus({
|
const status = getBridgeStatus({
|
||||||
error: undefined,
|
error: undefined,
|
||||||
connected,
|
connected,
|
||||||
sessionActive,
|
sessionActive,
|
||||||
reconnecting
|
reconnecting,
|
||||||
});
|
})
|
||||||
|
|
||||||
// For implicit (config-driven) remote, only show the reconnecting state
|
// For implicit (config-driven) remote, only show the reconnecting state
|
||||||
if (!explicit && status.label !== 'Remote Control reconnecting') {
|
if (!explicit && status.label !== 'Remote Control reconnecting') {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
return <Text color={bridgeSelected ? 'background' : status.color} inverse={bridgeSelected} wrap="truncate">
|
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
color={bridgeSelected ? 'background' : status.color}
|
||||||
|
inverse={bridgeSelected}
|
||||||
|
wrap="truncate"
|
||||||
|
>
|
||||||
{status.label}
|
{status.label}
|
||||||
{bridgeSelected && <Text dimColor> · Enter to view</Text>}
|
{bridgeSelected && <Text dimColor> · Enter to view</Text>}
|
||||||
</Text>;
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,292 +1,248 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { memo, type ReactNode } from 'react'
|
||||||
import { memo, type ReactNode } from 'react';
|
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
import { stringWidth } from '../../ink/stringWidth.js'
|
||||||
import { stringWidth } from '../../ink/stringWidth.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js'
|
||||||
import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js';
|
import type { Theme } from '../../utils/theme.js'
|
||||||
import type { Theme } from '../../utils/theme.js';
|
|
||||||
export type SuggestionItem = {
|
export type SuggestionItem = {
|
||||||
id: string;
|
id: string
|
||||||
displayText: string;
|
displayText: string
|
||||||
tag?: string;
|
tag?: string
|
||||||
description?: string;
|
description?: string
|
||||||
metadata?: unknown;
|
metadata?: unknown
|
||||||
color?: keyof Theme;
|
color?: keyof Theme
|
||||||
};
|
}
|
||||||
export type SuggestionType = 'command' | 'file' | 'directory' | 'agent' | 'shell' | 'custom-title' | 'slack-channel' | 'none';
|
|
||||||
export const OVERLAY_MAX_ITEMS = 5;
|
export type SuggestionType =
|
||||||
|
| 'command'
|
||||||
|
| 'file'
|
||||||
|
| 'directory'
|
||||||
|
| 'agent'
|
||||||
|
| 'shell'
|
||||||
|
| 'custom-title'
|
||||||
|
| 'slack-channel'
|
||||||
|
| 'none'
|
||||||
|
|
||||||
|
export const OVERLAY_MAX_ITEMS = 5
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the icon for a suggestion based on its type
|
* Get the icon for a suggestion based on its type
|
||||||
* Icons: + for files, ◇ for MCP resources, * for agents
|
* Icons: + for files, ◇ for MCP resources, * for agents
|
||||||
*/
|
*/
|
||||||
function getIcon(itemId: string): string {
|
function getIcon(itemId: string): string {
|
||||||
if (itemId.startsWith('file-')) return '+';
|
if (itemId.startsWith('file-')) return '+'
|
||||||
if (itemId.startsWith('mcp-resource-')) return '◇';
|
if (itemId.startsWith('mcp-resource-')) return '◇'
|
||||||
if (itemId.startsWith('agent-')) return '*';
|
if (itemId.startsWith('agent-')) return '*'
|
||||||
return '+';
|
return '+'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an item is a unified suggestion type (file, mcp-resource, or agent)
|
* Check if an item is a unified suggestion type (file, mcp-resource, or agent)
|
||||||
*/
|
*/
|
||||||
function isUnifiedSuggestion(itemId: string): boolean {
|
function isUnifiedSuggestion(itemId: string): boolean {
|
||||||
return itemId.startsWith('file-') || itemId.startsWith('mcp-resource-') || itemId.startsWith('agent-');
|
return (
|
||||||
|
itemId.startsWith('file-') ||
|
||||||
|
itemId.startsWith('mcp-resource-') ||
|
||||||
|
itemId.startsWith('agent-')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const SuggestionItemRow = memo(function SuggestionItemRow(t0: { item: SuggestionItem; maxColumnWidth: number; isSelected: boolean }) {
|
|
||||||
const $ = _c(36);
|
const SuggestionItemRow = memo(function SuggestionItemRow({
|
||||||
const {
|
item,
|
||||||
item,
|
maxColumnWidth,
|
||||||
maxColumnWidth,
|
isSelected,
|
||||||
isSelected
|
}: {
|
||||||
} = t0;
|
item: SuggestionItem
|
||||||
const columns = useTerminalSize().columns;
|
maxColumnWidth?: number
|
||||||
const isUnified = isUnifiedSuggestion(item.id);
|
isSelected: boolean
|
||||||
|
}): ReactNode {
|
||||||
|
const columns = useTerminalSize().columns
|
||||||
|
const isUnified = isUnifiedSuggestion(item.id)
|
||||||
|
|
||||||
|
// For unified suggestions (file, mcp-resource, agent), use single-line layout with icon
|
||||||
if (isUnified) {
|
if (isUnified) {
|
||||||
let t1;
|
const icon = getIcon(item.id)
|
||||||
if ($[0] !== item.id) {
|
const textColor: keyof Theme | undefined = isSelected
|
||||||
t1 = getIcon(item.id);
|
? 'suggestion'
|
||||||
$[0] = item.id;
|
: undefined
|
||||||
$[1] = t1;
|
const dimColor = !isSelected
|
||||||
} else {
|
|
||||||
t1 = $[1];
|
const isFile = item.id.startsWith('file-')
|
||||||
}
|
const isMcpResource = item.id.startsWith('mcp-resource-')
|
||||||
const icon = t1;
|
|
||||||
const textColor = isSelected ? "suggestion" : undefined;
|
// Calculate layout widths
|
||||||
const dimColor = !isSelected;
|
// Layout: "X " (2) + displayText + " – " (3) + description + padding (4)
|
||||||
const isFile = item.id.startsWith("file-");
|
const iconWidth = 2 // icon + space (fixed)
|
||||||
const isMcpResource = item.id.startsWith("mcp-resource-");
|
const paddingWidth = 4
|
||||||
const separatorWidth = item.description ? 3 : 0;
|
const separatorWidth = item.description ? 3 : 0 // ' – ' separator
|
||||||
let displayText;
|
|
||||||
|
// For files, truncate middle of path to show both directory context and filename
|
||||||
|
// For MCP resources, limit displayText to 30 chars (truncate from end)
|
||||||
|
// For agents, no truncation
|
||||||
|
let displayText: string
|
||||||
if (isFile) {
|
if (isFile) {
|
||||||
let t2;
|
// Reserve space for description if present, otherwise use all available space
|
||||||
if ($[2] !== item.description) {
|
const descReserve = item.description
|
||||||
t2 = item.description ? Math.min(20, stringWidth(item.description)) : 0;
|
? Math.min(20, stringWidth(item.description))
|
||||||
$[2] = item.description;
|
: 0
|
||||||
$[3] = t2;
|
const maxPathLength =
|
||||||
} else {
|
columns - iconWidth - paddingWidth - separatorWidth - descReserve
|
||||||
t2 = $[3];
|
displayText = truncatePathMiddle(item.displayText, maxPathLength)
|
||||||
}
|
} else if (isMcpResource) {
|
||||||
const descReserve = t2;
|
const maxDisplayTextLength = 30
|
||||||
const maxPathLength = columns - 2 - 4 - separatorWidth - descReserve;
|
displayText = truncateToWidth(item.displayText, maxDisplayTextLength)
|
||||||
let t3;
|
|
||||||
if ($[4] !== item.displayText || $[5] !== maxPathLength) {
|
|
||||||
t3 = truncatePathMiddle(item.displayText, maxPathLength);
|
|
||||||
$[4] = item.displayText;
|
|
||||||
$[5] = maxPathLength;
|
|
||||||
$[6] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[6];
|
|
||||||
}
|
|
||||||
displayText = t3;
|
|
||||||
} else {
|
} else {
|
||||||
if (isMcpResource) {
|
displayText = item.displayText
|
||||||
let t2;
|
|
||||||
if ($[7] !== item.displayText) {
|
|
||||||
t2 = truncateToWidth(item.displayText, 30);
|
|
||||||
$[7] = item.displayText;
|
|
||||||
$[8] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[8];
|
|
||||||
}
|
|
||||||
displayText = t2;
|
|
||||||
} else {
|
|
||||||
displayText = item.displayText;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const availableWidth = columns - 2 - stringWidth(displayText) - separatorWidth - 4;
|
|
||||||
let lineContent;
|
const availableWidth =
|
||||||
|
columns -
|
||||||
|
iconWidth -
|
||||||
|
stringWidth(displayText) -
|
||||||
|
separatorWidth -
|
||||||
|
paddingWidth
|
||||||
|
|
||||||
|
// Build the full line as a single string to prevent wrapping
|
||||||
|
let lineContent: string
|
||||||
if (item.description) {
|
if (item.description) {
|
||||||
const maxDescLength = Math.max(0, availableWidth);
|
const maxDescLength = Math.max(0, availableWidth)
|
||||||
let t2;
|
const truncatedDesc = truncateToWidth(
|
||||||
if ($[9] !== item.description || $[10] !== maxDescLength) {
|
item.description.replace(/\s+/g, ' '),
|
||||||
t2 = truncateToWidth(item.description.replace(/\s+/g, " "), maxDescLength);
|
maxDescLength,
|
||||||
$[9] = item.description;
|
)
|
||||||
$[10] = maxDescLength;
|
lineContent = `${icon} ${displayText} – ${truncatedDesc}`
|
||||||
$[11] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[11];
|
|
||||||
}
|
|
||||||
const truncatedDesc = t2;
|
|
||||||
lineContent = `${icon} ${displayText} – ${truncatedDesc}`;
|
|
||||||
} else {
|
} else {
|
||||||
lineContent = `${icon} ${displayText}`;
|
lineContent = `${icon} ${displayText}`
|
||||||
}
|
}
|
||||||
let t2;
|
|
||||||
if ($[12] !== dimColor || $[13] !== lineContent || $[14] !== textColor) {
|
return (
|
||||||
t2 = <Text color={textColor} dimColor={dimColor} wrap="truncate">{lineContent}</Text>;
|
<Text color={textColor} dimColor={dimColor} wrap="truncate">
|
||||||
$[12] = dimColor;
|
{lineContent}
|
||||||
$[13] = lineContent;
|
</Text>
|
||||||
$[14] = textColor;
|
)
|
||||||
$[15] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[15];
|
|
||||||
}
|
|
||||||
return t2;
|
|
||||||
}
|
}
|
||||||
const maxNameWidth = Math.floor(columns * 0.4);
|
|
||||||
const displayTextWidth = Math.min(maxColumnWidth ?? stringWidth(item.displayText) + 5, maxNameWidth);
|
// For non-unified suggestions (commands, shell, etc.), use improved layout from main
|
||||||
const textColor_0 = item.color || (isSelected ? "suggestion" : undefined);
|
// Cap the command name column at 40% of terminal width to ensure description has space
|
||||||
const shouldDim = !isSelected;
|
const maxNameWidth = Math.floor(columns * 0.4)
|
||||||
let displayText_0 = item.displayText;
|
const displayTextWidth = Math.min(
|
||||||
if (stringWidth(displayText_0) > displayTextWidth - 2) {
|
maxColumnWidth ?? stringWidth(item.displayText) + 5,
|
||||||
const t1 = displayTextWidth - 2;
|
maxNameWidth,
|
||||||
let t2;
|
)
|
||||||
if ($[16] !== displayText_0 || $[17] !== t1) {
|
|
||||||
t2 = truncateToWidth(displayText_0, t1);
|
const textColor = item.color || (isSelected ? 'suggestion' : undefined)
|
||||||
$[16] = displayText_0;
|
const shouldDim = !isSelected
|
||||||
$[17] = t1;
|
|
||||||
$[18] = t2;
|
// Truncate and pad the display text to fixed width
|
||||||
} else {
|
let displayText = item.displayText
|
||||||
t2 = $[18];
|
if (stringWidth(displayText) > displayTextWidth - 2) {
|
||||||
}
|
displayText = truncateToWidth(displayText, displayTextWidth - 2)
|
||||||
displayText_0 = t2;
|
|
||||||
}
|
}
|
||||||
const paddedDisplayText = displayText_0 + " ".repeat(Math.max(0, displayTextWidth - stringWidth(displayText_0)));
|
const paddedDisplayText =
|
||||||
const tagText = item.tag ? `[${item.tag}] ` : "";
|
displayText +
|
||||||
const tagWidth = stringWidth(tagText);
|
' '.repeat(Math.max(0, displayTextWidth - stringWidth(displayText)))
|
||||||
const descriptionWidth = Math.max(0, columns - displayTextWidth - tagWidth - 4);
|
|
||||||
let t1;
|
const tagText = item.tag ? `[${item.tag}] ` : ''
|
||||||
if ($[19] !== descriptionWidth || $[20] !== item.description) {
|
const tagWidth = stringWidth(tagText)
|
||||||
t1 = item.description ? truncateToWidth(item.description.replace(/\s+/g, " "), descriptionWidth) : "";
|
const descriptionWidth = Math.max(
|
||||||
$[19] = descriptionWidth;
|
0,
|
||||||
$[20] = item.description;
|
columns - displayTextWidth - tagWidth - 4,
|
||||||
$[21] = t1;
|
)
|
||||||
} else {
|
// Skill descriptions can contain newlines (e.g. /claude-api's "TRIGGER
|
||||||
t1 = $[21];
|
// when:" block). A multi-line row grows the overlay past minHeight; when
|
||||||
}
|
// the filter narrows past that skill, the overlay shrinks and leaves
|
||||||
const truncatedDescription = t1;
|
// ghost rows. Flatten to one line before truncating.
|
||||||
let t2;
|
const truncatedDescription = item.description
|
||||||
if ($[22] !== paddedDisplayText || $[23] !== shouldDim || $[24] !== textColor_0) {
|
? truncateToWidth(item.description.replace(/\s+/g, ' '), descriptionWidth)
|
||||||
t2 = <Text color={textColor_0} dimColor={shouldDim}>{paddedDisplayText}</Text>;
|
: ''
|
||||||
$[22] = paddedDisplayText;
|
|
||||||
$[23] = shouldDim;
|
return (
|
||||||
$[24] = textColor_0;
|
<Text wrap="truncate">
|
||||||
$[25] = t2;
|
<Text color={textColor} dimColor={shouldDim}>
|
||||||
} else {
|
{paddedDisplayText}
|
||||||
t2 = $[25];
|
</Text>
|
||||||
}
|
{tagText ? <Text dimColor>{tagText}</Text> : null}
|
||||||
let t3;
|
<Text
|
||||||
if ($[26] !== tagText) {
|
color={isSelected ? 'suggestion' : undefined}
|
||||||
t3 = tagText ? <Text dimColor={true}>{tagText}</Text> : null;
|
dimColor={!isSelected}
|
||||||
$[26] = tagText;
|
>
|
||||||
$[27] = t3;
|
{truncatedDescription}
|
||||||
} else {
|
</Text>
|
||||||
t3 = $[27];
|
</Text>
|
||||||
}
|
)
|
||||||
const t4 = isSelected ? "suggestion" : undefined;
|
})
|
||||||
const t5 = !isSelected;
|
|
||||||
let t6;
|
|
||||||
if ($[28] !== t4 || $[29] !== t5 || $[30] !== truncatedDescription) {
|
|
||||||
t6 = <Text color={t4} dimColor={t5}>{truncatedDescription}</Text>;
|
|
||||||
$[28] = t4;
|
|
||||||
$[29] = t5;
|
|
||||||
$[30] = truncatedDescription;
|
|
||||||
$[31] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[31];
|
|
||||||
}
|
|
||||||
let t7;
|
|
||||||
if ($[32] !== t2 || $[33] !== t3 || $[34] !== t6) {
|
|
||||||
t7 = <Text wrap="truncate">{t2}{t3}{t6}</Text>;
|
|
||||||
$[32] = t2;
|
|
||||||
$[33] = t3;
|
|
||||||
$[34] = t6;
|
|
||||||
$[35] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[35];
|
|
||||||
}
|
|
||||||
return t7;
|
|
||||||
});
|
|
||||||
type Props = {
|
type Props = {
|
||||||
suggestions: SuggestionItem[];
|
suggestions: SuggestionItem[]
|
||||||
selectedSuggestion: number;
|
selectedSuggestion: number
|
||||||
maxColumnWidth?: number;
|
maxColumnWidth?: number
|
||||||
/**
|
/**
|
||||||
* When true, the suggestions are rendered inside a position=absolute
|
* When true, the suggestions are rendered inside a position=absolute
|
||||||
* overlay. We omit minHeight and flex-end so the y-clamp in the
|
* overlay. We omit minHeight and flex-end so the y-clamp in the
|
||||||
* renderer doesn't push fewer items down into the prompt area.
|
* renderer doesn't push fewer items down into the prompt area.
|
||||||
*/
|
*/
|
||||||
overlay?: boolean;
|
overlay?: boolean
|
||||||
};
|
}
|
||||||
export function PromptInputFooterSuggestions(t0) {
|
|
||||||
const $ = _c(22);
|
export function PromptInputFooterSuggestions({
|
||||||
const {
|
suggestions,
|
||||||
suggestions,
|
selectedSuggestion,
|
||||||
selectedSuggestion,
|
maxColumnWidth: maxColumnWidthProp,
|
||||||
maxColumnWidth: maxColumnWidthProp,
|
overlay,
|
||||||
overlay
|
}: Props): ReactNode {
|
||||||
} = t0;
|
const { rows } = useTerminalSize()
|
||||||
const {
|
// Maximum number of suggestions to show at once (leaving space for prompt).
|
||||||
rows
|
// Overlay mode (fullscreen) uses a fixed 5 — the floating box sits over
|
||||||
} = useTerminalSize();
|
// the ScrollBox, so terminal height isn't the constraint.
|
||||||
const maxVisibleItems = overlay ? OVERLAY_MAX_ITEMS : Math.min(6, Math.max(1, rows - 3));
|
const maxVisibleItems = overlay
|
||||||
|
? OVERLAY_MAX_ITEMS
|
||||||
|
: Math.min(6, Math.max(1, rows - 3))
|
||||||
|
|
||||||
|
// No suggestions to display
|
||||||
if (suggestions.length === 0) {
|
if (suggestions.length === 0) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
let t1;
|
|
||||||
if ($[0] !== maxColumnWidthProp || $[1] !== suggestions) {
|
// Use prop if provided (stable width from all commands), otherwise calculate from visible
|
||||||
t1 = maxColumnWidthProp ?? Math.max(...suggestions.map(_temp)) + 5;
|
const maxColumnWidth =
|
||||||
$[0] = maxColumnWidthProp;
|
maxColumnWidthProp ??
|
||||||
$[1] = suggestions;
|
Math.max(...suggestions.map(item => stringWidth(item.displayText))) + 5
|
||||||
$[2] = t1;
|
|
||||||
} else {
|
// Calculate visible items range based on selected index
|
||||||
t1 = $[2];
|
const startIndex = Math.max(
|
||||||
}
|
0,
|
||||||
const maxColumnWidth = t1;
|
Math.min(
|
||||||
const startIndex = Math.max(0, Math.min(selectedSuggestion - Math.floor(maxVisibleItems / 2), suggestions.length - maxVisibleItems));
|
selectedSuggestion - Math.floor(maxVisibleItems / 2),
|
||||||
const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length);
|
suggestions.length - maxVisibleItems,
|
||||||
let T0;
|
),
|
||||||
let t2;
|
)
|
||||||
let t3;
|
const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length)
|
||||||
let t4;
|
const visibleItems = suggestions.slice(startIndex, endIndex)
|
||||||
if ($[3] !== endIndex || $[4] !== maxColumnWidth || $[5] !== overlay || $[6] !== selectedSuggestion || $[7] !== startIndex || $[8] !== suggestions) {
|
|
||||||
const visibleItems = suggestions.slice(startIndex, endIndex);
|
// In non-overlay (inline) mode, justifyContent keeps suggestions
|
||||||
T0 = Box;
|
// anchored to the bottom (near the prompt). In overlay mode we omit
|
||||||
t2 = "column";
|
// both minHeight and flex-end: the parent is position=absolute with
|
||||||
t3 = overlay ? undefined : "flex-end";
|
// bottom='100%', so its y is clamped to 0 by the renderer when it
|
||||||
let t5;
|
// would go negative. Adding minHeight + flex-end would create empty
|
||||||
if ($[13] !== maxColumnWidth || $[14] !== selectedSuggestion || $[15] !== suggestions) {
|
// padding rows that shift the visible items down into the prompt area
|
||||||
t5 = item_0 => <SuggestionItemRow key={item_0.id} item={item_0} maxColumnWidth={maxColumnWidth} isSelected={item_0.id === suggestions[selectedSuggestion]?.id} />;
|
// when the list has fewer items than maxVisibleItems.
|
||||||
$[13] = maxColumnWidth;
|
return (
|
||||||
$[14] = selectedSuggestion;
|
<Box
|
||||||
$[15] = suggestions;
|
flexDirection="column"
|
||||||
$[16] = t5;
|
justifyContent={overlay ? undefined : 'flex-end'}
|
||||||
} else {
|
>
|
||||||
t5 = $[16];
|
{visibleItems.map(item => (
|
||||||
}
|
<SuggestionItemRow
|
||||||
t4 = visibleItems.map(t5);
|
key={item.id}
|
||||||
$[3] = endIndex;
|
item={item}
|
||||||
$[4] = maxColumnWidth;
|
maxColumnWidth={maxColumnWidth}
|
||||||
$[5] = overlay;
|
isSelected={item.id === suggestions[selectedSuggestion]?.id}
|
||||||
$[6] = selectedSuggestion;
|
/>
|
||||||
$[7] = startIndex;
|
))}
|
||||||
$[8] = suggestions;
|
</Box>
|
||||||
$[9] = T0;
|
)
|
||||||
$[10] = t2;
|
|
||||||
$[11] = t3;
|
|
||||||
$[12] = t4;
|
|
||||||
} else {
|
|
||||||
T0 = $[9];
|
|
||||||
t2 = $[10];
|
|
||||||
t3 = $[11];
|
|
||||||
t4 = $[12];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[17] !== T0 || $[18] !== t2 || $[19] !== t3 || $[20] !== t4) {
|
|
||||||
t5 = <T0 flexDirection={t2} justifyContent={t3}>{t4}</T0>;
|
|
||||||
$[17] = T0;
|
|
||||||
$[18] = t2;
|
|
||||||
$[19] = t3;
|
|
||||||
$[20] = t4;
|
|
||||||
$[21] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[21];
|
|
||||||
}
|
|
||||||
return t5;
|
|
||||||
}
|
}
|
||||||
function _temp(item) {
|
|
||||||
return stringWidth(item.displayText);
|
export default memo(PromptInputFooterSuggestions)
|
||||||
}
|
|
||||||
export default memo(PromptInputFooterSuggestions);
|
|
||||||
|
|||||||
@@ -1,357 +1,149 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import { feature } from 'bun:bundle'
|
||||||
import { feature } from 'bun:bundle';
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { Box, Text } from 'src/ink.js'
|
||||||
import { Box, Text } from 'src/ink.js';
|
import { getPlatform } from 'src/utils/platform.js'
|
||||||
import { getPlatform } from 'src/utils/platform.js';
|
import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js'
|
||||||
import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js';
|
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
|
||||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
|
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js';
|
import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js'
|
||||||
import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js';
|
import { getNewlineInstructions } from './utils.js'
|
||||||
import { getNewlineInstructions } from './utils.js';
|
|
||||||
|
|
||||||
/** Format a shortcut for display in the help menu (e.g., "ctrl+o" → "ctrl + o") */
|
/** Format a shortcut for display in the help menu (e.g., "ctrl+o" → "ctrl + o") */
|
||||||
function formatShortcut(shortcut: string): string {
|
function formatShortcut(shortcut: string): string {
|
||||||
return shortcut.replace(/\+/g, ' + ');
|
return shortcut.replace(/\+/g, ' + ')
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
dimColor?: boolean;
|
dimColor?: boolean
|
||||||
fixedWidth?: boolean;
|
fixedWidth?: boolean
|
||||||
gap?: number;
|
gap?: number
|
||||||
paddingX?: number;
|
paddingX?: number
|
||||||
};
|
}
|
||||||
export function PromptInputHelpMenu(props) {
|
|
||||||
const $ = _c(99);
|
export function PromptInputHelpMenu(props: Props): React.ReactNode {
|
||||||
const {
|
const { dimColor, fixedWidth, gap, paddingX } = props
|
||||||
dimColor,
|
|
||||||
fixedWidth,
|
// Get configured shortcuts from keybinding system
|
||||||
gap,
|
const transcriptShortcut = formatShortcut(
|
||||||
paddingX
|
useShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o'),
|
||||||
} = props;
|
)
|
||||||
const t0 = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
|
const todosShortcut = formatShortcut(
|
||||||
let t1;
|
useShortcutDisplay('app:toggleTodos', 'Global', 'ctrl+t'),
|
||||||
if ($[0] !== t0) {
|
)
|
||||||
t1 = formatShortcut(t0);
|
const undoShortcut = formatShortcut(
|
||||||
$[0] = t0;
|
useShortcutDisplay('chat:undo', 'Chat', 'ctrl+_'),
|
||||||
$[1] = t1;
|
)
|
||||||
} else {
|
const stashShortcut = formatShortcut(
|
||||||
t1 = $[1];
|
useShortcutDisplay('chat:stash', 'Chat', 'ctrl+s'),
|
||||||
}
|
)
|
||||||
const transcriptShortcut = t1;
|
const cycleModeShortcut = formatShortcut(
|
||||||
const t2 = useShortcutDisplay("app:toggleTodos", "Global", "ctrl+t");
|
useShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab'),
|
||||||
let t3;
|
)
|
||||||
if ($[2] !== t2) {
|
const modelPickerShortcut = formatShortcut(
|
||||||
t3 = formatShortcut(t2);
|
useShortcutDisplay('chat:modelPicker', 'Chat', 'alt+p'),
|
||||||
$[2] = t2;
|
)
|
||||||
$[3] = t3;
|
const fastModeShortcut = formatShortcut(
|
||||||
} else {
|
useShortcutDisplay('chat:fastMode', 'Chat', 'alt+o'),
|
||||||
t3 = $[3];
|
)
|
||||||
}
|
const externalEditorShortcut = formatShortcut(
|
||||||
const todosShortcut = t3;
|
useShortcutDisplay('chat:externalEditor', 'Chat', 'ctrl+g'),
|
||||||
const t4 = useShortcutDisplay("chat:undo", "Chat", "ctrl+_");
|
)
|
||||||
let t5;
|
const terminalShortcut = formatShortcut(
|
||||||
if ($[4] !== t4) {
|
useShortcutDisplay('app:toggleTerminal', 'Global', 'meta+j'),
|
||||||
t5 = formatShortcut(t4);
|
)
|
||||||
$[4] = t4;
|
const imagePasteShortcut = formatShortcut(
|
||||||
$[5] = t5;
|
useShortcutDisplay('chat:imagePaste', 'Chat', 'ctrl+v'),
|
||||||
} else {
|
)
|
||||||
t5 = $[5];
|
|
||||||
}
|
// Compute terminal shortcut element outside JSX to satisfy feature() constraint
|
||||||
const undoShortcut = t5;
|
const terminalShortcutElement = feature('TERMINAL_PANEL') ? (
|
||||||
const t6 = useShortcutDisplay("chat:stash", "Chat", "ctrl+s");
|
getFeatureValue_CACHED_MAY_BE_STALE('tengu_terminal_panel', false) ? (
|
||||||
let t7;
|
<Box>
|
||||||
if ($[6] !== t6) {
|
<Text dimColor={dimColor}>{terminalShortcut} for terminal</Text>
|
||||||
t7 = formatShortcut(t6);
|
</Box>
|
||||||
$[6] = t6;
|
) : null
|
||||||
$[7] = t7;
|
) : null
|
||||||
} else {
|
|
||||||
t7 = $[7];
|
return (
|
||||||
}
|
<Box paddingX={paddingX} flexDirection="row" gap={gap}>
|
||||||
const stashShortcut = t7;
|
<Box flexDirection="column" width={fixedWidth ? 24 : undefined}>
|
||||||
const t8 = useShortcutDisplay("chat:cycleMode", "Chat", "shift+tab");
|
<Box>
|
||||||
let t9;
|
<Text dimColor={dimColor}>! for bash mode</Text>
|
||||||
if ($[8] !== t8) {
|
</Box>
|
||||||
t9 = formatShortcut(t8);
|
<Box>
|
||||||
$[8] = t8;
|
<Text dimColor={dimColor}>/ for commands</Text>
|
||||||
$[9] = t9;
|
</Box>
|
||||||
} else {
|
<Box>
|
||||||
t9 = $[9];
|
<Text dimColor={dimColor}>@ for file paths</Text>
|
||||||
}
|
</Box>
|
||||||
const cycleModeShortcut = t9;
|
<Box>
|
||||||
const t10 = useShortcutDisplay("chat:modelPicker", "Chat", "alt+p");
|
<Text dimColor={dimColor}>& for background</Text>
|
||||||
let t11;
|
</Box>
|
||||||
if ($[10] !== t10) {
|
<Box>
|
||||||
t11 = formatShortcut(t10);
|
<Text dimColor={dimColor}>/btw for side question</Text>
|
||||||
$[10] = t10;
|
</Box>
|
||||||
$[11] = t11;
|
</Box>
|
||||||
} else {
|
<Box flexDirection="column" width={fixedWidth ? 35 : undefined}>
|
||||||
t11 = $[11];
|
<Box>
|
||||||
}
|
<Text dimColor={dimColor}>double tap esc to clear input</Text>
|
||||||
const modelPickerShortcut = t11;
|
</Box>
|
||||||
const t12 = useShortcutDisplay("chat:fastMode", "Chat", "alt+o");
|
<Box>
|
||||||
let t13;
|
<Text dimColor={dimColor}>
|
||||||
if ($[12] !== t12) {
|
{cycleModeShortcut}{' '}
|
||||||
t13 = formatShortcut(t12);
|
{process.env.USER_TYPE === 'ant'
|
||||||
$[12] = t12;
|
? 'to cycle modes'
|
||||||
$[13] = t13;
|
: 'to auto-accept edits'}
|
||||||
} else {
|
</Text>
|
||||||
t13 = $[13];
|
</Box>
|
||||||
}
|
<Box>
|
||||||
const fastModeShortcut = t13;
|
<Text dimColor={dimColor}>
|
||||||
const t14 = useShortcutDisplay("chat:externalEditor", "Chat", "ctrl+g");
|
{transcriptShortcut} for verbose output
|
||||||
let t15;
|
</Text>
|
||||||
if ($[14] !== t14) {
|
</Box>
|
||||||
t15 = formatShortcut(t14);
|
<Box>
|
||||||
$[14] = t14;
|
<Text dimColor={dimColor}>{todosShortcut} to toggle tasks</Text>
|
||||||
$[15] = t15;
|
</Box>
|
||||||
} else {
|
{terminalShortcutElement}
|
||||||
t15 = $[15];
|
<Box>
|
||||||
}
|
<Text dimColor={dimColor}>{getNewlineInstructions()}</Text>
|
||||||
const externalEditorShortcut = t15;
|
</Box>
|
||||||
const t16 = useShortcutDisplay("app:toggleTerminal", "Global", "meta+j");
|
</Box>
|
||||||
let t17;
|
<Box flexDirection="column">
|
||||||
if ($[16] !== t16) {
|
<Box>
|
||||||
t17 = formatShortcut(t16);
|
<Text dimColor={dimColor}>{undoShortcut} to undo</Text>
|
||||||
$[16] = t16;
|
</Box>
|
||||||
$[17] = t17;
|
{getPlatform() !== 'windows' && (
|
||||||
} else {
|
<Box>
|
||||||
t17 = $[17];
|
<Text dimColor={dimColor}>ctrl + z to suspend</Text>
|
||||||
}
|
</Box>
|
||||||
const terminalShortcut = t17;
|
)}
|
||||||
const t18 = useShortcutDisplay("chat:imagePaste", "Chat", "ctrl+v");
|
<Box>
|
||||||
let t19;
|
<Text dimColor={dimColor}>{imagePasteShortcut} to paste images</Text>
|
||||||
if ($[18] !== t18) {
|
</Box>
|
||||||
t19 = formatShortcut(t18);
|
<Box>
|
||||||
$[18] = t18;
|
<Text dimColor={dimColor}>{modelPickerShortcut} to switch model</Text>
|
||||||
$[19] = t19;
|
</Box>
|
||||||
} else {
|
{isFastModeEnabled() && isFastModeAvailable() && (
|
||||||
t19 = $[19];
|
<Box>
|
||||||
}
|
<Text dimColor={dimColor}>
|
||||||
const imagePasteShortcut = t19;
|
{fastModeShortcut} to toggle fast mode
|
||||||
let t20;
|
</Text>
|
||||||
if ($[20] !== dimColor || $[21] !== terminalShortcut) {
|
</Box>
|
||||||
t20 = feature("TERMINAL_PANEL") ? getFeatureValue_CACHED_MAY_BE_STALE("tengu_terminal_panel", false) ? <Box><Text dimColor={dimColor}>{terminalShortcut} for terminal</Text></Box> : null : null;
|
)}
|
||||||
$[20] = dimColor;
|
<Box>
|
||||||
$[21] = terminalShortcut;
|
<Text dimColor={dimColor}>{stashShortcut} to stash prompt</Text>
|
||||||
$[22] = t20;
|
</Box>
|
||||||
} else {
|
<Box>
|
||||||
t20 = $[22];
|
<Text dimColor={dimColor}>
|
||||||
}
|
{externalEditorShortcut} to edit in $EDITOR
|
||||||
const terminalShortcutElement = t20;
|
</Text>
|
||||||
const t21 = fixedWidth ? 24 : undefined;
|
</Box>
|
||||||
let t22;
|
{isKeybindingCustomizationEnabled() && (
|
||||||
if ($[23] !== dimColor) {
|
<Box>
|
||||||
t22 = <Box><Text dimColor={dimColor}>! for bash mode</Text></Box>;
|
<Text dimColor={dimColor}>/keybindings to customize</Text>
|
||||||
$[23] = dimColor;
|
</Box>
|
||||||
$[24] = t22;
|
)}
|
||||||
} else {
|
</Box>
|
||||||
t22 = $[24];
|
</Box>
|
||||||
}
|
)
|
||||||
let t23;
|
|
||||||
if ($[25] !== dimColor) {
|
|
||||||
t23 = <Box><Text dimColor={dimColor}>/ for commands</Text></Box>;
|
|
||||||
$[25] = dimColor;
|
|
||||||
$[26] = t23;
|
|
||||||
} else {
|
|
||||||
t23 = $[26];
|
|
||||||
}
|
|
||||||
let t24;
|
|
||||||
if ($[27] !== dimColor) {
|
|
||||||
t24 = <Box><Text dimColor={dimColor}>@ for file paths</Text></Box>;
|
|
||||||
$[27] = dimColor;
|
|
||||||
$[28] = t24;
|
|
||||||
} else {
|
|
||||||
t24 = $[28];
|
|
||||||
}
|
|
||||||
let t25;
|
|
||||||
if ($[29] !== dimColor) {
|
|
||||||
t25 = <Box><Text dimColor={dimColor}>{"& for background"}</Text></Box>;
|
|
||||||
$[29] = dimColor;
|
|
||||||
$[30] = t25;
|
|
||||||
} else {
|
|
||||||
t25 = $[30];
|
|
||||||
}
|
|
||||||
let t26;
|
|
||||||
if ($[31] !== dimColor) {
|
|
||||||
t26 = <Box><Text dimColor={dimColor}>/btw for side question</Text></Box>;
|
|
||||||
$[31] = dimColor;
|
|
||||||
$[32] = t26;
|
|
||||||
} else {
|
|
||||||
t26 = $[32];
|
|
||||||
}
|
|
||||||
let t27;
|
|
||||||
if ($[33] !== t21 || $[34] !== t22 || $[35] !== t23 || $[36] !== t24 || $[37] !== t25 || $[38] !== t26) {
|
|
||||||
t27 = <Box flexDirection="column" width={t21}>{t22}{t23}{t24}{t25}{t26}</Box>;
|
|
||||||
$[33] = t21;
|
|
||||||
$[34] = t22;
|
|
||||||
$[35] = t23;
|
|
||||||
$[36] = t24;
|
|
||||||
$[37] = t25;
|
|
||||||
$[38] = t26;
|
|
||||||
$[39] = t27;
|
|
||||||
} else {
|
|
||||||
t27 = $[39];
|
|
||||||
}
|
|
||||||
const t28 = fixedWidth ? 35 : undefined;
|
|
||||||
let t29;
|
|
||||||
if ($[40] !== dimColor) {
|
|
||||||
t29 = <Box><Text dimColor={dimColor}>double tap esc to clear input</Text></Box>;
|
|
||||||
$[40] = dimColor;
|
|
||||||
$[41] = t29;
|
|
||||||
} else {
|
|
||||||
t29 = $[41];
|
|
||||||
}
|
|
||||||
let t30;
|
|
||||||
if ($[42] !== cycleModeShortcut || $[43] !== dimColor) {
|
|
||||||
t30 = <Box><Text dimColor={dimColor}>{cycleModeShortcut}{" "}{false ? "to cycle modes" : "to auto-accept edits"}</Text></Box>;
|
|
||||||
$[42] = cycleModeShortcut;
|
|
||||||
$[43] = dimColor;
|
|
||||||
$[44] = t30;
|
|
||||||
} else {
|
|
||||||
t30 = $[44];
|
|
||||||
}
|
|
||||||
let t31;
|
|
||||||
if ($[45] !== dimColor || $[46] !== transcriptShortcut) {
|
|
||||||
t31 = <Box><Text dimColor={dimColor}>{transcriptShortcut} for verbose output</Text></Box>;
|
|
||||||
$[45] = dimColor;
|
|
||||||
$[46] = transcriptShortcut;
|
|
||||||
$[47] = t31;
|
|
||||||
} else {
|
|
||||||
t31 = $[47];
|
|
||||||
}
|
|
||||||
let t32;
|
|
||||||
if ($[48] !== dimColor || $[49] !== todosShortcut) {
|
|
||||||
t32 = <Box><Text dimColor={dimColor}>{todosShortcut} to toggle tasks</Text></Box>;
|
|
||||||
$[48] = dimColor;
|
|
||||||
$[49] = todosShortcut;
|
|
||||||
$[50] = t32;
|
|
||||||
} else {
|
|
||||||
t32 = $[50];
|
|
||||||
}
|
|
||||||
let t33;
|
|
||||||
if ($[51] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t33 = getNewlineInstructions();
|
|
||||||
$[51] = t33;
|
|
||||||
} else {
|
|
||||||
t33 = $[51];
|
|
||||||
}
|
|
||||||
let t34;
|
|
||||||
if ($[52] !== dimColor) {
|
|
||||||
t34 = <Box><Text dimColor={dimColor}>{t33}</Text></Box>;
|
|
||||||
$[52] = dimColor;
|
|
||||||
$[53] = t34;
|
|
||||||
} else {
|
|
||||||
t34 = $[53];
|
|
||||||
}
|
|
||||||
let t35;
|
|
||||||
if ($[54] !== t28 || $[55] !== t29 || $[56] !== t30 || $[57] !== t31 || $[58] !== t32 || $[59] !== t34 || $[60] !== terminalShortcutElement) {
|
|
||||||
t35 = <Box flexDirection="column" width={t28}>{t29}{t30}{t31}{t32}{terminalShortcutElement}{t34}</Box>;
|
|
||||||
$[54] = t28;
|
|
||||||
$[55] = t29;
|
|
||||||
$[56] = t30;
|
|
||||||
$[57] = t31;
|
|
||||||
$[58] = t32;
|
|
||||||
$[59] = t34;
|
|
||||||
$[60] = terminalShortcutElement;
|
|
||||||
$[61] = t35;
|
|
||||||
} else {
|
|
||||||
t35 = $[61];
|
|
||||||
}
|
|
||||||
let t36;
|
|
||||||
if ($[62] !== dimColor || $[63] !== undoShortcut) {
|
|
||||||
t36 = <Box><Text dimColor={dimColor}>{undoShortcut} to undo</Text></Box>;
|
|
||||||
$[62] = dimColor;
|
|
||||||
$[63] = undoShortcut;
|
|
||||||
$[64] = t36;
|
|
||||||
} else {
|
|
||||||
t36 = $[64];
|
|
||||||
}
|
|
||||||
let t37;
|
|
||||||
if ($[65] !== dimColor) {
|
|
||||||
t37 = getPlatform() !== "windows" && <Box><Text dimColor={dimColor}>ctrl + z to suspend</Text></Box>;
|
|
||||||
$[65] = dimColor;
|
|
||||||
$[66] = t37;
|
|
||||||
} else {
|
|
||||||
t37 = $[66];
|
|
||||||
}
|
|
||||||
let t38;
|
|
||||||
if ($[67] !== dimColor || $[68] !== imagePasteShortcut) {
|
|
||||||
t38 = <Box><Text dimColor={dimColor}>{imagePasteShortcut} to paste images</Text></Box>;
|
|
||||||
$[67] = dimColor;
|
|
||||||
$[68] = imagePasteShortcut;
|
|
||||||
$[69] = t38;
|
|
||||||
} else {
|
|
||||||
t38 = $[69];
|
|
||||||
}
|
|
||||||
let t39;
|
|
||||||
if ($[70] !== dimColor || $[71] !== modelPickerShortcut) {
|
|
||||||
t39 = <Box><Text dimColor={dimColor}>{modelPickerShortcut} to switch model</Text></Box>;
|
|
||||||
$[70] = dimColor;
|
|
||||||
$[71] = modelPickerShortcut;
|
|
||||||
$[72] = t39;
|
|
||||||
} else {
|
|
||||||
t39 = $[72];
|
|
||||||
}
|
|
||||||
let t40;
|
|
||||||
if ($[73] !== dimColor || $[74] !== fastModeShortcut) {
|
|
||||||
t40 = isFastModeEnabled() && isFastModeAvailable() && <Box><Text dimColor={dimColor}>{fastModeShortcut} to toggle fast mode</Text></Box>;
|
|
||||||
$[73] = dimColor;
|
|
||||||
$[74] = fastModeShortcut;
|
|
||||||
$[75] = t40;
|
|
||||||
} else {
|
|
||||||
t40 = $[75];
|
|
||||||
}
|
|
||||||
let t41;
|
|
||||||
if ($[76] !== dimColor || $[77] !== stashShortcut) {
|
|
||||||
t41 = <Box><Text dimColor={dimColor}>{stashShortcut} to stash prompt</Text></Box>;
|
|
||||||
$[76] = dimColor;
|
|
||||||
$[77] = stashShortcut;
|
|
||||||
$[78] = t41;
|
|
||||||
} else {
|
|
||||||
t41 = $[78];
|
|
||||||
}
|
|
||||||
let t42;
|
|
||||||
if ($[79] !== dimColor || $[80] !== externalEditorShortcut) {
|
|
||||||
t42 = <Box><Text dimColor={dimColor}>{externalEditorShortcut} to edit in $EDITOR</Text></Box>;
|
|
||||||
$[79] = dimColor;
|
|
||||||
$[80] = externalEditorShortcut;
|
|
||||||
$[81] = t42;
|
|
||||||
} else {
|
|
||||||
t42 = $[81];
|
|
||||||
}
|
|
||||||
let t43;
|
|
||||||
if ($[82] !== dimColor) {
|
|
||||||
t43 = isKeybindingCustomizationEnabled() && <Box><Text dimColor={dimColor}>/keybindings to customize</Text></Box>;
|
|
||||||
$[82] = dimColor;
|
|
||||||
$[83] = t43;
|
|
||||||
} else {
|
|
||||||
t43 = $[83];
|
|
||||||
}
|
|
||||||
let t44;
|
|
||||||
if ($[84] !== t36 || $[85] !== t37 || $[86] !== t38 || $[87] !== t39 || $[88] !== t40 || $[89] !== t41 || $[90] !== t42 || $[91] !== t43) {
|
|
||||||
t44 = <Box flexDirection="column">{t36}{t37}{t38}{t39}{t40}{t41}{t42}{t43}</Box>;
|
|
||||||
$[84] = t36;
|
|
||||||
$[85] = t37;
|
|
||||||
$[86] = t38;
|
|
||||||
$[87] = t39;
|
|
||||||
$[88] = t40;
|
|
||||||
$[89] = t41;
|
|
||||||
$[90] = t42;
|
|
||||||
$[91] = t43;
|
|
||||||
$[92] = t44;
|
|
||||||
} else {
|
|
||||||
t44 = $[92];
|
|
||||||
}
|
|
||||||
let t45;
|
|
||||||
if ($[93] !== gap || $[94] !== paddingX || $[95] !== t27 || $[96] !== t35 || $[97] !== t44) {
|
|
||||||
t45 = <Box paddingX={paddingX} flexDirection="row" gap={gap}>{t27}{t35}{t44}</Box>;
|
|
||||||
$[93] = gap;
|
|
||||||
$[94] = paddingX;
|
|
||||||
$[95] = t27;
|
|
||||||
$[96] = t35;
|
|
||||||
$[97] = t44;
|
|
||||||
$[98] = t45;
|
|
||||||
} else {
|
|
||||||
t45 = $[98];
|
|
||||||
}
|
|
||||||
return t45;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import figures from 'figures'
|
||||||
import figures from 'figures';
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { Box, Text } from 'src/ink.js'
|
||||||
import { Box, Text } from 'src/ink.js';
|
import {
|
||||||
import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from 'src/tools/AgentTool/agentColorManager.js';
|
AGENT_COLOR_TO_THEME_COLOR,
|
||||||
import type { PromptInputMode } from 'src/types/textInputTypes.js';
|
AGENT_COLORS,
|
||||||
import { getTeammateColor } from 'src/utils/teammate.js';
|
type AgentColorName,
|
||||||
import type { Theme } from 'src/utils/theme.js';
|
} from 'src/tools/AgentTool/agentColorManager.js'
|
||||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js';
|
import type { PromptInputMode } from 'src/types/textInputTypes.js'
|
||||||
|
import { getTeammateColor } from 'src/utils/teammate.js'
|
||||||
|
import type { Theme } from 'src/utils/theme.js'
|
||||||
|
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
mode: PromptInputMode;
|
mode: PromptInputMode
|
||||||
isLoading: boolean;
|
isLoading: boolean
|
||||||
viewingAgentName?: string;
|
viewingAgentName?: string
|
||||||
viewingAgentColor?: AgentColorName;
|
viewingAgentColor?: AgentColorName
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the theme color key for the teammate's assigned color.
|
* Gets the theme color key for the teammate's assigned color.
|
||||||
@@ -20,73 +24,81 @@ type Props = {
|
|||||||
*/
|
*/
|
||||||
function getTeammateThemeColor(): keyof Theme | undefined {
|
function getTeammateThemeColor(): keyof Theme | undefined {
|
||||||
if (!isAgentSwarmsEnabled()) {
|
if (!isAgentSwarmsEnabled()) {
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
const colorName = getTeammateColor();
|
const colorName = getTeammateColor()
|
||||||
if (!colorName) {
|
if (!colorName) {
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
if (AGENT_COLORS.includes(colorName as AgentColorName)) {
|
if (AGENT_COLORS.includes(colorName as AgentColorName)) {
|
||||||
return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName];
|
return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
type PromptCharProps = {
|
type PromptCharProps = {
|
||||||
isLoading: boolean;
|
isLoading: boolean
|
||||||
// Dead code elimination: parameter named themeColor to avoid "teammate" string in external builds
|
// Dead code elimination: parameter named themeColor to avoid "teammate" string in external builds
|
||||||
themeColor?: keyof Theme;
|
themeColor?: keyof Theme
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the prompt character (❯).
|
* Renders the prompt character (❯).
|
||||||
* Teammate color overrides the default color when set.
|
* Teammate color overrides the default color when set.
|
||||||
*/
|
*/
|
||||||
function PromptChar(t0) {
|
function PromptChar({
|
||||||
const $ = _c(3);
|
isLoading,
|
||||||
const {
|
themeColor,
|
||||||
isLoading,
|
}: PromptCharProps): React.ReactNode {
|
||||||
themeColor
|
// Assign to original name for clarity within the function
|
||||||
} = t0;
|
const teammateColor = themeColor
|
||||||
const teammateColor = themeColor;
|
const isAnt = process.env.USER_TYPE === 'ant'
|
||||||
const color = teammateColor ?? (false ? "subtle" : undefined);
|
const color = teammateColor ?? (isAnt ? 'subtle' : undefined)
|
||||||
let t1;
|
|
||||||
if ($[0] !== color || $[1] !== isLoading) {
|
return (
|
||||||
t1 = <Text color={color} dimColor={isLoading}>{figures.pointer} </Text>;
|
<Text color={color} dimColor={isLoading}>
|
||||||
$[0] = color;
|
{figures.pointer}
|
||||||
$[1] = isLoading;
|
</Text>
|
||||||
$[2] = t1;
|
)
|
||||||
} else {
|
|
||||||
t1 = $[2];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
}
|
||||||
export function PromptInputModeIndicator(t0) {
|
|
||||||
const $ = _c(6);
|
export function PromptInputModeIndicator({
|
||||||
const {
|
mode,
|
||||||
mode,
|
isLoading,
|
||||||
isLoading,
|
viewingAgentName,
|
||||||
viewingAgentName,
|
viewingAgentColor,
|
||||||
viewingAgentColor
|
}: Props): React.ReactNode {
|
||||||
} = t0;
|
const teammateColor = getTeammateThemeColor()
|
||||||
let t1;
|
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
// Convert viewed teammate's color to theme color
|
||||||
t1 = getTeammateThemeColor();
|
// Falls back to PromptChar's default (subtle for ants, undefined for external)
|
||||||
$[0] = t1;
|
const viewedTeammateThemeColor = viewingAgentColor
|
||||||
} else {
|
? AGENT_COLOR_TO_THEME_COLOR[viewingAgentColor]
|
||||||
t1 = $[0];
|
: undefined
|
||||||
}
|
|
||||||
const teammateColor = t1;
|
return (
|
||||||
const viewedTeammateThemeColor = viewingAgentColor ? AGENT_COLOR_TO_THEME_COLOR[viewingAgentColor] : undefined;
|
<Box
|
||||||
let t2;
|
alignItems="flex-start"
|
||||||
if ($[1] !== isLoading || $[2] !== mode || $[3] !== viewedTeammateThemeColor || $[4] !== viewingAgentName) {
|
alignSelf="flex-start"
|
||||||
t2 = <Box alignItems="flex-start" alignSelf="flex-start" flexWrap="nowrap" justifyContent="flex-start">{viewingAgentName ? <PromptChar isLoading={isLoading} themeColor={viewedTeammateThemeColor} /> : mode === "bash" ? <Text color="bashBorder" dimColor={isLoading}>! </Text> : <PromptChar isLoading={isLoading} themeColor={isAgentSwarmsEnabled() ? teammateColor : undefined} />}</Box>;
|
flexWrap="nowrap"
|
||||||
$[1] = isLoading;
|
justifyContent="flex-start"
|
||||||
$[2] = mode;
|
>
|
||||||
$[3] = viewedTeammateThemeColor;
|
{viewingAgentName ? (
|
||||||
$[4] = viewingAgentName;
|
// Use teammate's color on the standard prompt character, matching established style
|
||||||
$[5] = t2;
|
<PromptChar
|
||||||
} else {
|
isLoading={isLoading}
|
||||||
t2 = $[5];
|
themeColor={viewedTeammateThemeColor}
|
||||||
}
|
/>
|
||||||
return t2;
|
) : mode === 'bash' ? (
|
||||||
|
<Text color="bashBorder" dimColor={isLoading}>
|
||||||
|
!
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<PromptChar
|
||||||
|
isLoading={isLoading}
|
||||||
|
themeColor={isAgentSwarmsEnabled() ? teammateColor : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
import { feature } from 'bun:bundle';
|
import { feature } from 'bun:bundle'
|
||||||
import * as React from 'react';
|
import * as React from 'react'
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react'
|
||||||
import { Box } from 'src/ink.js';
|
import { Box } from 'src/ink.js'
|
||||||
import { useAppState } from 'src/state/AppState.js';
|
import { useAppState } from 'src/state/AppState.js'
|
||||||
import { STATUS_TAG, SUMMARY_TAG, TASK_NOTIFICATION_TAG } from '../../constants/xml.js';
|
import {
|
||||||
import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js';
|
STATUS_TAG,
|
||||||
import { useCommandQueue } from '../../hooks/useCommandQueue.js';
|
SUMMARY_TAG,
|
||||||
import type { QueuedCommand } from '../../types/textInputTypes.js';
|
TASK_NOTIFICATION_TAG,
|
||||||
import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js';
|
} from '../../constants/xml.js'
|
||||||
import { createUserMessage, EMPTY_LOOKUPS, normalizeMessages } from '../../utils/messages.js';
|
import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js'
|
||||||
import { jsonParse } from '../../utils/slowOperations.js';
|
import { useCommandQueue } from '../../hooks/useCommandQueue.js'
|
||||||
import { Message } from '../Message.js';
|
import type { QueuedCommand } from '../../types/textInputTypes.js'
|
||||||
const EMPTY_SET = new Set<string>();
|
import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js'
|
||||||
|
import {
|
||||||
|
createUserMessage,
|
||||||
|
EMPTY_LOOKUPS,
|
||||||
|
normalizeMessages,
|
||||||
|
} from '../../utils/messages.js'
|
||||||
|
import { jsonParse } from '../../utils/slowOperations.js'
|
||||||
|
import { Message } from '../Message.js'
|
||||||
|
|
||||||
|
const EMPTY_SET = new Set<string>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a command value is an idle notification that should be hidden.
|
* Check if a command value is an idle notification that should be hidden.
|
||||||
@@ -19,15 +28,15 @@ const EMPTY_SET = new Set<string>();
|
|||||||
*/
|
*/
|
||||||
function isIdleNotification(value: string): boolean {
|
function isIdleNotification(value: string): boolean {
|
||||||
try {
|
try {
|
||||||
const parsed = jsonParse(value);
|
const parsed = jsonParse(value)
|
||||||
return parsed?.type === 'idle_notification';
|
return parsed?.type === 'idle_notification'
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maximum number of task notification lines to show
|
// Maximum number of task notification lines to show
|
||||||
const MAX_VISIBLE_NOTIFICATIONS = 3;
|
const MAX_VISIBLE_NOTIFICATIONS = 3
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a synthetic overflow notification message for capped task notifications.
|
* Create a synthetic overflow notification message for capped task notifications.
|
||||||
@@ -36,7 +45,7 @@ function createOverflowNotificationMessage(count: number): string {
|
|||||||
return `<${TASK_NOTIFICATION_TAG}>
|
return `<${TASK_NOTIFICATION_TAG}>
|
||||||
<${SUMMARY_TAG}>+${count} more tasks completed</${SUMMARY_TAG}>
|
<${SUMMARY_TAG}>+${count} more tasks completed</${SUMMARY_TAG}>
|
||||||
<${STATUS_TAG}>completed</${STATUS_TAG}>
|
<${STATUS_TAG}>completed</${STATUS_TAG}>
|
||||||
</${TASK_NOTIFICATION_TAG}>`;
|
</${TASK_NOTIFICATION_TAG}>`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,73 +53,114 @@ function createOverflowNotificationMessage(count: number): string {
|
|||||||
* Other command types are always shown in full.
|
* Other command types are always shown in full.
|
||||||
* Idle notifications are filtered out entirely.
|
* Idle notifications are filtered out entirely.
|
||||||
*/
|
*/
|
||||||
function processQueuedCommands(queuedCommands: QueuedCommand[]): QueuedCommand[] {
|
function processQueuedCommands(
|
||||||
|
queuedCommands: QueuedCommand[],
|
||||||
|
): QueuedCommand[] {
|
||||||
// Filter out idle notifications - they are processed silently
|
// Filter out idle notifications - they are processed silently
|
||||||
const filteredCommands = queuedCommands.filter(cmd => typeof cmd.value !== 'string' || !isIdleNotification(cmd.value));
|
const filteredCommands = queuedCommands.filter(
|
||||||
|
cmd => typeof cmd.value !== 'string' || !isIdleNotification(cmd.value),
|
||||||
|
)
|
||||||
|
|
||||||
// Separate task notifications from other commands
|
// Separate task notifications from other commands
|
||||||
const taskNotifications = filteredCommands.filter(cmd => cmd.mode === 'task-notification');
|
const taskNotifications = filteredCommands.filter(
|
||||||
const otherCommands = filteredCommands.filter(cmd => cmd.mode !== 'task-notification');
|
cmd => cmd.mode === 'task-notification',
|
||||||
|
)
|
||||||
|
const otherCommands = filteredCommands.filter(
|
||||||
|
cmd => cmd.mode !== 'task-notification',
|
||||||
|
)
|
||||||
|
|
||||||
// If notifications fit within limit, return all commands as-is
|
// If notifications fit within limit, return all commands as-is
|
||||||
if (taskNotifications.length <= MAX_VISIBLE_NOTIFICATIONS) {
|
if (taskNotifications.length <= MAX_VISIBLE_NOTIFICATIONS) {
|
||||||
return [...otherCommands, ...taskNotifications];
|
return [...otherCommands, ...taskNotifications]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show first (MAX_VISIBLE_NOTIFICATIONS - 1) notifications, then a summary
|
// Show first (MAX_VISIBLE_NOTIFICATIONS - 1) notifications, then a summary
|
||||||
const visibleNotifications = taskNotifications.slice(0, MAX_VISIBLE_NOTIFICATIONS - 1);
|
const visibleNotifications = taskNotifications.slice(
|
||||||
const overflowCount = taskNotifications.length - (MAX_VISIBLE_NOTIFICATIONS - 1);
|
0,
|
||||||
|
MAX_VISIBLE_NOTIFICATIONS - 1,
|
||||||
|
)
|
||||||
|
const overflowCount =
|
||||||
|
taskNotifications.length - (MAX_VISIBLE_NOTIFICATIONS - 1)
|
||||||
|
|
||||||
// Create synthetic overflow message
|
// Create synthetic overflow message
|
||||||
const overflowCommand: QueuedCommand = {
|
const overflowCommand: QueuedCommand = {
|
||||||
value: createOverflowNotificationMessage(overflowCount),
|
value: createOverflowNotificationMessage(overflowCount),
|
||||||
mode: 'task-notification'
|
mode: 'task-notification',
|
||||||
};
|
}
|
||||||
return [...otherCommands, ...visibleNotifications, overflowCommand];
|
|
||||||
|
return [...otherCommands, ...visibleNotifications, overflowCommand]
|
||||||
}
|
}
|
||||||
|
|
||||||
function PromptInputQueuedCommandsImpl(): React.ReactNode {
|
function PromptInputQueuedCommandsImpl(): React.ReactNode {
|
||||||
const queuedCommands = useCommandQueue();
|
const queuedCommands = useCommandQueue()
|
||||||
const viewingAgent = useAppState(s => !!s.viewingAgentTaskId);
|
const viewingAgent = useAppState(s => !!s.viewingAgentTaskId)
|
||||||
// Brief layout: dim queue items + skip the paddingX (brief messages
|
// Brief layout: dim queue items + skip the paddingX (brief messages
|
||||||
// already indent themselves). Gate mirrors the brief-spinner/message
|
// already indent themselves). Gate mirrors the brief-spinner/message
|
||||||
// check elsewhere — no teammate-view override needed since this
|
// check elsewhere — no teammate-view override needed since this
|
||||||
// component early-returns when viewing a teammate.
|
// component early-returns when viewing a teammate.
|
||||||
const useBriefLayout = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
const useBriefLayout =
|
||||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||||
useAppState(s_0 => s_0.isBriefOnly) : false;
|
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||||
|
useAppState(s => s.isBriefOnly)
|
||||||
|
: false
|
||||||
|
|
||||||
// createUserMessage mints a fresh UUID per call; without memoization, streaming
|
// createUserMessage mints a fresh UUID per call; without memoization, streaming
|
||||||
// re-renders defeat Message's areMessagePropsEqual (compares uuid) → flicker.
|
// re-renders defeat Message's areMessagePropsEqual (compares uuid) → flicker.
|
||||||
const messages = useMemo(() => {
|
const messages = useMemo(() => {
|
||||||
if (queuedCommands.length === 0) return null;
|
if (queuedCommands.length === 0) return null
|
||||||
// task-notification is shown via useInboxNotification; most isMeta commands
|
// task-notification is shown via useInboxNotification; most isMeta commands
|
||||||
// (scheduled tasks, proactive ticks) are system-generated and hidden.
|
// (scheduled tasks, proactive ticks) are system-generated and hidden.
|
||||||
// Channel messages are the exception — isMeta but shown so the keyboard
|
// Channel messages are the exception — isMeta but shown so the keyboard
|
||||||
// user sees what arrived.
|
// user sees what arrived.
|
||||||
const visibleCommands = queuedCommands.filter(isQueuedCommandVisible);
|
const visibleCommands = queuedCommands.filter(isQueuedCommandVisible)
|
||||||
if (visibleCommands.length === 0) return null;
|
if (visibleCommands.length === 0) return null
|
||||||
const processedCommands = processQueuedCommands(visibleCommands);
|
const processedCommands = processQueuedCommands(visibleCommands)
|
||||||
return normalizeMessages(processedCommands.map(cmd => {
|
return normalizeMessages(
|
||||||
let content = cmd.value;
|
processedCommands.map(cmd => {
|
||||||
if (cmd.mode === 'bash' && typeof content === 'string') {
|
let content = cmd.value
|
||||||
content = `<bash-input>${content}</bash-input>`;
|
if (cmd.mode === 'bash' && typeof content === 'string') {
|
||||||
}
|
content = `<bash-input>${content}</bash-input>`
|
||||||
// [Image #N] placeholders are inline in the text value (inserted at
|
}
|
||||||
// paste time), so the queue preview shows them without stub blocks.
|
// [Image #N] placeholders are inline in the text value (inserted at
|
||||||
return createUserMessage({
|
// paste time), so the queue preview shows them without stub blocks.
|
||||||
content
|
return createUserMessage({ content })
|
||||||
});
|
}),
|
||||||
}));
|
)
|
||||||
}, [queuedCommands]);
|
}, [queuedCommands])
|
||||||
|
|
||||||
// Don't show leader's queued commands when viewing any agent's transcript
|
// Don't show leader's queued commands when viewing any agent's transcript
|
||||||
if (viewingAgent || messages === null) {
|
if (viewingAgent || messages === null) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
return <Box marginTop={1} flexDirection="column">
|
|
||||||
{messages.map((message, i) => <QueuedMessageProvider key={i} isFirst={i === 0} useBriefLayout={useBriefLayout}>
|
return (
|
||||||
<Message message={message} lookups={EMPTY_LOOKUPS} addMargin={false} tools={[]} commands={[]} verbose={false} inProgressToolUseIDs={EMPTY_SET} progressMessagesForMessage={[]} shouldAnimate={false} shouldShowDot={false} isTranscriptMode={false} isStatic={true} />
|
<Box marginTop={1} flexDirection="column">
|
||||||
</QueuedMessageProvider>)}
|
{messages.map((message, i) => (
|
||||||
</Box>;
|
<QueuedMessageProvider
|
||||||
|
key={i}
|
||||||
|
isFirst={i === 0}
|
||||||
|
useBriefLayout={useBriefLayout}
|
||||||
|
>
|
||||||
|
<Message
|
||||||
|
message={message}
|
||||||
|
lookups={EMPTY_LOOKUPS}
|
||||||
|
addMargin={false}
|
||||||
|
tools={[]}
|
||||||
|
commands={[]}
|
||||||
|
verbose={false}
|
||||||
|
inProgressToolUseIDs={EMPTY_SET}
|
||||||
|
progressMessagesForMessage={[]}
|
||||||
|
shouldAnimate={false}
|
||||||
|
shouldShowDot={false}
|
||||||
|
isTranscriptMode={false}
|
||||||
|
isStatic={true}
|
||||||
|
/>
|
||||||
|
</QueuedMessageProvider>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
export const PromptInputQueuedCommands = React.memo(PromptInputQueuedCommandsImpl);
|
|
||||||
|
export const PromptInputQueuedCommands = React.memo(
|
||||||
|
PromptInputQueuedCommandsImpl,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import figures from 'figures'
|
||||||
import figures from 'figures';
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { Box, Text } from 'src/ink.js'
|
||||||
import { Box, Text } from 'src/ink.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
hasStash: boolean;
|
hasStash: boolean
|
||||||
};
|
}
|
||||||
export function PromptInputStashNotice(t0) {
|
|
||||||
const $ = _c(1);
|
export function PromptInputStashNotice({ hasStash }: Props): React.ReactNode {
|
||||||
const {
|
if (!hasStash) {
|
||||||
hasStash
|
return null
|
||||||
} = t0;
|
}
|
||||||
if (!hasStash) {
|
|
||||||
return null;
|
return (
|
||||||
}
|
<Box paddingLeft={2}>
|
||||||
let t1;
|
<Text dimColor>
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
{figures.pointerSmall} Stashed (auto-restores after submit)
|
||||||
t1 = <Box paddingLeft={2}><Text dimColor={true}>{figures.pointerSmall} Stashed (auto-restores after submit)</Text></Box>;
|
</Text>
|
||||||
$[0] = t1;
|
</Box>
|
||||||
} else {
|
)
|
||||||
t1 = $[0];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,61 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { type ReactNode, useEffect, useRef, useState } from 'react'
|
||||||
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
|
||||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
|
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
|
|
||||||
export function SandboxPromptFooterHint() {
|
export function SandboxPromptFooterHint(): ReactNode {
|
||||||
const $ = _c(6);
|
const [recentViolationCount, setRecentViolationCount] = useState(0)
|
||||||
const [recentViolationCount, setRecentViolationCount] = useState(0);
|
const timerRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
const timerRef = useRef(null);
|
const detailsShortcut = useShortcutDisplay(
|
||||||
const detailsShortcut = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
|
'app:toggleTranscript',
|
||||||
let t0;
|
'Global',
|
||||||
let t1;
|
'ctrl+o',
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
)
|
||||||
t0 = () => {
|
|
||||||
if (!SandboxManager.isSandboxingEnabled()) {
|
useEffect(() => {
|
||||||
return;
|
if (!SandboxManager.isSandboxingEnabled()) {
|
||||||
}
|
return
|
||||||
const store = SandboxManager.getSandboxViolationStore();
|
}
|
||||||
let lastCount = store.getTotalCount();
|
|
||||||
const unsubscribe = store.subscribe(() => {
|
const store = SandboxManager.getSandboxViolationStore()
|
||||||
const currentCount = store.getTotalCount();
|
let lastCount = store.getTotalCount()
|
||||||
const newViolations = currentCount - lastCount;
|
|
||||||
if (newViolations > 0) {
|
const unsubscribe = store.subscribe(() => {
|
||||||
setRecentViolationCount(newViolations);
|
const currentCount = store.getTotalCount()
|
||||||
lastCount = currentCount;
|
const newViolations = currentCount - lastCount
|
||||||
if (timerRef.current) {
|
|
||||||
clearTimeout(timerRef.current);
|
if (newViolations > 0) {
|
||||||
}
|
setRecentViolationCount(newViolations)
|
||||||
timerRef.current = setTimeout(setRecentViolationCount, 5000, 0);
|
lastCount = currentCount
|
||||||
}
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
if (timerRef.current) {
|
if (timerRef.current) {
|
||||||
clearTimeout(timerRef.current);
|
clearTimeout(timerRef.current)
|
||||||
}
|
}
|
||||||
};
|
|
||||||
};
|
timerRef.current = setTimeout(setRecentViolationCount, 5000, 0)
|
||||||
t1 = [];
|
}
|
||||||
$[0] = t0;
|
})
|
||||||
$[1] = t1;
|
|
||||||
} else {
|
return () => {
|
||||||
t0 = $[0];
|
unsubscribe()
|
||||||
t1 = $[1];
|
if (timerRef.current) {
|
||||||
}
|
clearTimeout(timerRef.current)
|
||||||
useEffect(t0, t1);
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
if (!SandboxManager.isSandboxingEnabled() || recentViolationCount === 0) {
|
if (!SandboxManager.isSandboxingEnabled() || recentViolationCount === 0) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
const t2 = recentViolationCount === 1 ? "operation" : "operations";
|
|
||||||
let t3;
|
return (
|
||||||
if ($[2] !== detailsShortcut || $[3] !== recentViolationCount || $[4] !== t2) {
|
<Box paddingX={0} paddingY={0}>
|
||||||
t3 = <Box paddingX={0} paddingY={0}><Text color="inactive" wrap="truncate">⧈ Sandbox blocked {recentViolationCount}{" "}{t2} ·{" "}{detailsShortcut} for details · /sandbox to disable</Text></Box>;
|
<Text color="inactive" wrap="truncate">
|
||||||
$[2] = detailsShortcut;
|
⧈ Sandbox blocked {recentViolationCount}{' '}
|
||||||
$[3] = recentViolationCount;
|
{recentViolationCount === 1 ? 'operation' : 'operations'} ·{' '}
|
||||||
$[4] = t2;
|
{detailsShortcut} for details · /sandbox to disable
|
||||||
$[5] = t3;
|
</Text>
|
||||||
} else {
|
</Box>
|
||||||
t3 = $[5];
|
)
|
||||||
}
|
|
||||||
return t3;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,142 +1,121 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { Ansi, Box, Text, useAnimationFrame } from '../../ink.js'
|
||||||
import { Ansi, Box, Text, useAnimationFrame } from '../../ink.js';
|
import {
|
||||||
import { segmentTextByHighlights, type TextHighlight } from '../../utils/textHighlighting.js';
|
segmentTextByHighlights,
|
||||||
import { ShimmerChar } from '../Spinner/ShimmerChar.js';
|
type TextHighlight,
|
||||||
|
} from '../../utils/textHighlighting.js'
|
||||||
|
import { ShimmerChar } from '../Spinner/ShimmerChar.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
text: string;
|
text: string
|
||||||
highlights: TextHighlight[];
|
highlights: TextHighlight[]
|
||||||
};
|
}
|
||||||
|
|
||||||
type LinePart = {
|
type LinePart = {
|
||||||
text: string;
|
text: string
|
||||||
highlight: TextHighlight | undefined;
|
highlight: TextHighlight | undefined
|
||||||
start: number;
|
start: number
|
||||||
};
|
}
|
||||||
export function HighlightedInput(t0) {
|
|
||||||
const $ = _c(23);
|
export function HighlightedInput({ text, highlights }: Props): React.ReactNode {
|
||||||
const {
|
// The shimmer animation (below) re-renders this component at 20fps while the
|
||||||
text,
|
// ultrathink keyword is present. text/highlights are referentially stable
|
||||||
highlights
|
// across animation ticks (parent doesn't re-render), so memoize everything
|
||||||
} = t0;
|
// that derives from them: segmentTextByHighlights alone is ~85µs/call
|
||||||
let lines;
|
// (tokenize + sort + O(n²) overlap), which adds up fast at 20fps.
|
||||||
if ($[0] !== highlights || $[1] !== text) {
|
const { lines, hasShimmer, sweepStart, cycleLength } = React.useMemo(() => {
|
||||||
const segments = segmentTextByHighlights(text, highlights);
|
const segments = segmentTextByHighlights(text, highlights)
|
||||||
lines = [[]];
|
|
||||||
let pos = 0;
|
// Split segments by newlines into per-line groups. Ink's row-direction Box
|
||||||
|
// indents continuation lines of a multi-line child to that child's X offset.
|
||||||
|
// By splitting at newlines, each line renders as its own row, avoiding the
|
||||||
|
// incorrect indentation when highlighted text is followed by wrapped content.
|
||||||
|
const lines: LinePart[][] = [[]]
|
||||||
|
let pos = 0
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
const parts = segment.text.split("\n");
|
const parts = segment.text.split('\n')
|
||||||
for (let i = 0; i < parts.length; i++) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
lines.push([]);
|
lines.push([])
|
||||||
pos = pos + 1;
|
pos += 1
|
||||||
}
|
}
|
||||||
const part = parts[i];
|
const part = parts[i]!
|
||||||
if (part.length > 0) {
|
if (part.length > 0) {
|
||||||
lines[lines.length - 1].push({
|
lines[lines.length - 1]!.push({
|
||||||
text: part,
|
text: part,
|
||||||
highlight: segment.highlight,
|
highlight: segment.highlight,
|
||||||
start: pos
|
start: pos,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
pos = pos + part.length;
|
pos += part.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$[0] = highlights;
|
|
||||||
$[1] = text;
|
// Scope the sweep to shimmer-highlighted ranges so cycle time doesn't grow
|
||||||
$[2] = lines;
|
// with input length. Padding creates an offscreen pause between sweeps.
|
||||||
} else {
|
const hasShimmer = highlights.some(h => h.shimmerColor)
|
||||||
lines = $[2];
|
let sweepStart = 0
|
||||||
}
|
let cycleLength = 1
|
||||||
let t1;
|
if (hasShimmer) {
|
||||||
if ($[3] !== highlights) {
|
const padding = 10
|
||||||
t1 = highlights.some(_temp);
|
let lo = Infinity
|
||||||
$[3] = highlights;
|
let hi = -Infinity
|
||||||
$[4] = t1;
|
for (const h of highlights) {
|
||||||
} else {
|
if (h.shimmerColor) {
|
||||||
t1 = $[4];
|
lo = Math.min(lo, h.start)
|
||||||
}
|
hi = Math.max(hi, h.end)
|
||||||
const hasShimmer = t1;
|
|
||||||
let sweepStart = 0;
|
|
||||||
let cycleLength = 1;
|
|
||||||
if (hasShimmer) {
|
|
||||||
let lo = Infinity;
|
|
||||||
let hi = -Infinity;
|
|
||||||
if ($[5] !== hi || $[6] !== highlights || $[7] !== lo) {
|
|
||||||
for (const h_0 of highlights) {
|
|
||||||
if (h_0.shimmerColor) {
|
|
||||||
lo = Math.min(lo, h_0.start);
|
|
||||||
hi = Math.max(hi, h_0.end);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$[5] = hi;
|
sweepStart = lo - padding
|
||||||
$[6] = highlights;
|
cycleLength = hi - lo + padding * 2
|
||||||
$[7] = lo;
|
|
||||||
$[8] = lo;
|
|
||||||
$[9] = hi;
|
|
||||||
} else {
|
|
||||||
lo = $[8] as number;
|
|
||||||
hi = $[9] as number;
|
|
||||||
}
|
}
|
||||||
sweepStart = lo - 10;
|
|
||||||
cycleLength = hi - lo + 20;
|
return { lines, hasShimmer, sweepStart, cycleLength }
|
||||||
}
|
}, [text, highlights])
|
||||||
let t2;
|
|
||||||
if ($[10] !== cycleLength || $[11] !== hasShimmer || $[12] !== lines || $[13] !== sweepStart) {
|
const [ref, time] = useAnimationFrame(hasShimmer ? 50 : null)
|
||||||
t2 = {
|
const glimmerIndex = hasShimmer
|
||||||
lines,
|
? sweepStart + (Math.floor(time / 50) % cycleLength)
|
||||||
hasShimmer,
|
: -100
|
||||||
sweepStart,
|
|
||||||
cycleLength
|
return (
|
||||||
};
|
<Box ref={ref} flexDirection="column">
|
||||||
$[10] = cycleLength;
|
{lines.map((lineParts, lineIndex) => (
|
||||||
$[11] = hasShimmer;
|
<Box key={lineIndex}>
|
||||||
$[12] = lines;
|
{lineParts.length === 0 ? (
|
||||||
$[13] = sweepStart;
|
<Text> </Text>
|
||||||
$[14] = t2;
|
) : (
|
||||||
} else {
|
lineParts.map((part, partIndex) => {
|
||||||
t2 = $[14];
|
if (part.highlight?.shimmerColor && part.highlight.color) {
|
||||||
}
|
return (
|
||||||
const {
|
<Text key={partIndex}>
|
||||||
lines: lines_0,
|
{part.text.split('').map((char, charIndex) => (
|
||||||
hasShimmer: hasShimmer_0,
|
<ShimmerChar
|
||||||
sweepStart: sweepStart_0,
|
key={charIndex}
|
||||||
cycleLength: cycleLength_0
|
char={char}
|
||||||
} = t2;
|
index={part.start + charIndex}
|
||||||
const [ref, time] = useAnimationFrame(hasShimmer_0 ? 50 : null);
|
glimmerIndex={glimmerIndex}
|
||||||
const glimmerIndex = hasShimmer_0 ? sweepStart_0 + Math.floor(time / 50) % cycleLength_0 : -100;
|
messageColor={part.highlight!.color!}
|
||||||
let t3;
|
shimmerColor={part.highlight!.shimmerColor!}
|
||||||
if ($[15] !== glimmerIndex || $[16] !== lines_0) {
|
/>
|
||||||
let t4;
|
))}
|
||||||
if ($[18] !== glimmerIndex) {
|
</Text>
|
||||||
t4 = (lineParts, lineIndex) => <Box key={lineIndex}>{lineParts.length === 0 ? <Text> </Text> : lineParts.map((part_0, partIndex) => {
|
)
|
||||||
if (part_0.highlight?.shimmerColor && part_0.highlight.color) {
|
}
|
||||||
return <Text key={partIndex}>{part_0.text.split("").map((char, charIndex) => <ShimmerChar key={charIndex} char={char} index={part_0.start + charIndex} glimmerIndex={glimmerIndex} messageColor={part_0.highlight.color} shimmerColor={part_0.highlight.shimmerColor} />)}</Text>;
|
return (
|
||||||
}
|
<Text
|
||||||
return <Text key={partIndex} color={part_0.highlight?.color} dimColor={part_0.highlight?.dimColor} inverse={part_0.highlight?.inverse}><Ansi>{part_0.text}</Ansi></Text>;
|
key={partIndex}
|
||||||
})}</Box>;
|
color={part.highlight?.color}
|
||||||
$[18] = glimmerIndex;
|
dimColor={part.highlight?.dimColor}
|
||||||
$[19] = t4;
|
inverse={part.highlight?.inverse}
|
||||||
} else {
|
>
|
||||||
t4 = $[19];
|
<Ansi>{part.text}</Ansi>
|
||||||
}
|
</Text>
|
||||||
t3 = lines_0.map(t4);
|
)
|
||||||
$[15] = glimmerIndex;
|
})
|
||||||
$[16] = lines_0;
|
)}
|
||||||
$[17] = t3;
|
</Box>
|
||||||
} else {
|
))}
|
||||||
t3 = $[17];
|
</Box>
|
||||||
}
|
)
|
||||||
let t4;
|
|
||||||
if ($[20] !== ref || $[21] !== t3) {
|
|
||||||
t4 = <Box ref={ref} flexDirection="column">{t3}</Box>;
|
|
||||||
$[20] = ref;
|
|
||||||
$[21] = t3;
|
|
||||||
$[22] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[22];
|
|
||||||
}
|
|
||||||
return t4;
|
|
||||||
}
|
|
||||||
function _temp(h) {
|
|
||||||
return h.shimmerColor;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,73 +1,32 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import { feature } from 'bun:bundle'
|
||||||
import { feature } from 'bun:bundle';
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { useSettings } from '../../hooks/useSettings.js'
|
||||||
import { useSettings } from '../../hooks/useSettings.js';
|
import { Box, Text, useAnimationFrame } from '../../ink.js'
|
||||||
import { Box, Text, useAnimationFrame } from '../../ink.js';
|
import { interpolateColor, toRGBColor } from '../Spinner/utils.js'
|
||||||
import { interpolateColor, toRGBColor } from '../Spinner/utils.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
voiceState: 'idle' | 'recording' | 'processing';
|
voiceState: 'idle' | 'recording' | 'processing'
|
||||||
};
|
}
|
||||||
|
|
||||||
// Processing shimmer colors: dim gray to lighter gray (matches ThinkingShimmerText)
|
// Processing shimmer colors: dim gray to lighter gray (matches ThinkingShimmerText)
|
||||||
const PROCESSING_DIM = {
|
const PROCESSING_DIM = { r: 153, g: 153, b: 153 }
|
||||||
r: 153,
|
const PROCESSING_BRIGHT = { r: 185, g: 185, b: 185 }
|
||||||
g: 153,
|
|
||||||
b: 153
|
|
||||||
};
|
|
||||||
const PROCESSING_BRIGHT = {
|
|
||||||
r: 185,
|
|
||||||
g: 185,
|
|
||||||
b: 185
|
|
||||||
};
|
|
||||||
const PULSE_PERIOD_S = 2; // 2 second period for all pulsing animations
|
|
||||||
|
|
||||||
export function VoiceIndicator(props) {
|
const PULSE_PERIOD_S = 2 // 2 second period for all pulsing animations
|
||||||
const $ = _c(2);
|
|
||||||
if (!feature("VOICE_MODE")) {
|
export function VoiceIndicator(props: Props): React.ReactNode {
|
||||||
return null;
|
if (!feature('VOICE_MODE')) return null
|
||||||
}
|
return <VoiceIndicatorImpl {...props} />
|
||||||
let t0;
|
|
||||||
if ($[0] !== props) {
|
|
||||||
t0 = <VoiceIndicatorImpl {...props} />;
|
|
||||||
$[0] = props;
|
|
||||||
$[1] = t0;
|
|
||||||
} else {
|
|
||||||
t0 = $[1];
|
|
||||||
}
|
|
||||||
return t0;
|
|
||||||
}
|
}
|
||||||
function VoiceIndicatorImpl(t0) {
|
|
||||||
const $ = _c(2);
|
function VoiceIndicatorImpl({ voiceState }: Props): React.ReactNode {
|
||||||
const {
|
|
||||||
voiceState
|
|
||||||
} = t0;
|
|
||||||
switch (voiceState) {
|
switch (voiceState) {
|
||||||
case "recording":
|
case 'recording':
|
||||||
{
|
return <Text dimColor>listening…</Text>
|
||||||
let t1;
|
case 'processing':
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
return <ProcessingShimmer />
|
||||||
t1 = <Text dimColor={true}>listening…</Text>;
|
case 'idle':
|
||||||
$[0] = t1;
|
return null
|
||||||
} else {
|
|
||||||
t1 = $[0];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
|
||||||
case "processing":
|
|
||||||
{
|
|
||||||
let t1;
|
|
||||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t1 = <ProcessingShimmer />;
|
|
||||||
$[1] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[1];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
|
||||||
case "idle":
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,62 +34,30 @@ function VoiceIndicatorImpl(t0) {
|
|||||||
// is too brief for a 1s-period shimmer to register, and a 50ms animation
|
// is too brief for a 1s-period shimmer to register, and a 50ms animation
|
||||||
// timer here runs concurrently with auto-repeat spaces arriving every
|
// timer here runs concurrently with auto-repeat spaces arriving every
|
||||||
// 30-80ms, compounding re-renders during an already-busy window.
|
// 30-80ms, compounding re-renders during an already-busy window.
|
||||||
export function VoiceWarmupHint() {
|
export function VoiceWarmupHint(): React.ReactNode {
|
||||||
const $ = _c(1);
|
if (!feature('VOICE_MODE')) return null
|
||||||
if (!feature("VOICE_MODE")) {
|
return <Text dimColor>keep holding…</Text>
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let t0;
|
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t0 = <Text dimColor={true}>keep holding…</Text>;
|
|
||||||
$[0] = t0;
|
|
||||||
} else {
|
|
||||||
t0 = $[0];
|
|
||||||
}
|
|
||||||
return t0;
|
|
||||||
}
|
}
|
||||||
function ProcessingShimmer() {
|
|
||||||
const $ = _c(8);
|
function ProcessingShimmer(): React.ReactNode {
|
||||||
const settings = useSettings();
|
const settings = useSettings()
|
||||||
const reducedMotion = settings.prefersReducedMotion ?? false;
|
const reducedMotion = settings.prefersReducedMotion ?? false
|
||||||
const [ref, time] = useAnimationFrame(reducedMotion ? null : 50);
|
const [ref, time] = useAnimationFrame(reducedMotion ? null : 50)
|
||||||
|
|
||||||
if (reducedMotion) {
|
if (reducedMotion) {
|
||||||
let t0;
|
return <Text color="warning">Voice: processing…</Text>
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t0 = <Text color="warning">Voice: processing…</Text>;
|
|
||||||
$[0] = t0;
|
|
||||||
} else {
|
|
||||||
t0 = $[0];
|
|
||||||
}
|
|
||||||
return t0;
|
|
||||||
}
|
}
|
||||||
const elapsedSec = time / 1000;
|
|
||||||
const opacity = (Math.sin(elapsedSec * Math.PI * 2 / PULSE_PERIOD_S) + 1) / 2;
|
const elapsedSec = time / 1000
|
||||||
let t0;
|
const opacity =
|
||||||
if ($[1] !== opacity) {
|
(Math.sin((elapsedSec * Math.PI * 2) / PULSE_PERIOD_S) + 1) / 2
|
||||||
t0 = toRGBColor(interpolateColor(PROCESSING_DIM, PROCESSING_BRIGHT, opacity));
|
const color = toRGBColor(
|
||||||
$[1] = opacity;
|
interpolateColor(PROCESSING_DIM, PROCESSING_BRIGHT, opacity),
|
||||||
$[2] = t0;
|
)
|
||||||
} else {
|
|
||||||
t0 = $[2];
|
return (
|
||||||
}
|
<Box ref={ref}>
|
||||||
const color = t0;
|
<Text color={color}>Voice: processing…</Text>
|
||||||
let t1;
|
</Box>
|
||||||
if ($[3] !== color) {
|
)
|
||||||
t1 = <Text color={color}>Voice: processing…</Text>;
|
|
||||||
$[3] = color;
|
|
||||||
$[4] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[4];
|
|
||||||
}
|
|
||||||
let t2;
|
|
||||||
if ($[5] !== ref || $[6] !== t1) {
|
|
||||||
t2 = <Box ref={ref}>{t1}</Box>;
|
|
||||||
$[5] = ref;
|
|
||||||
$[6] = t1;
|
|
||||||
$[7] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[7];
|
|
||||||
}
|
|
||||||
return t2;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,219 +1,148 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import figures from 'figures'
|
||||||
import figures from 'figures';
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
import type { Tools } from '../../Tool.js'
|
||||||
import type { Tools } from '../../Tool.js';
|
import { getAgentColor } from '../../tools/AgentTool/agentColorManager.js'
|
||||||
import { getAgentColor } from '../../tools/AgentTool/agentColorManager.js';
|
import { getMemoryScopeDisplay } from '../../tools/AgentTool/agentMemory.js'
|
||||||
import { getMemoryScopeDisplay } from '../../tools/AgentTool/agentMemory.js';
|
import { resolveAgentTools } from '../../tools/AgentTool/agentToolUtils.js'
|
||||||
import { resolveAgentTools } from '../../tools/AgentTool/agentToolUtils.js';
|
import {
|
||||||
import { type AgentDefinition, isBuiltInAgent } from '../../tools/AgentTool/loadAgentsDir.js';
|
type AgentDefinition,
|
||||||
import { getAgentModelDisplay } from '../../utils/model/agent.js';
|
isBuiltInAgent,
|
||||||
import { Markdown } from '../Markdown.js';
|
} from '../../tools/AgentTool/loadAgentsDir.js'
|
||||||
import { getActualRelativeAgentFilePath } from './agentFileUtils.js';
|
import { getAgentModelDisplay } from '../../utils/model/agent.js'
|
||||||
|
import { Markdown } from '../Markdown.js'
|
||||||
|
import { getActualRelativeAgentFilePath } from './agentFileUtils.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
agent: AgentDefinition;
|
agent: AgentDefinition
|
||||||
tools: Tools;
|
tools: Tools
|
||||||
allAgents?: AgentDefinition[];
|
allAgents?: AgentDefinition[]
|
||||||
onBack: () => void;
|
onBack: () => void
|
||||||
};
|
}
|
||||||
export function AgentDetail(t0) {
|
|
||||||
const $ = _c(48);
|
export function AgentDetail({ agent, tools, onBack }: Props): React.ReactNode {
|
||||||
const {
|
const resolvedTools = resolveAgentTools(agent, tools, false)
|
||||||
agent,
|
const filePath = getActualRelativeAgentFilePath(agent)
|
||||||
tools,
|
const backgroundColor = getAgentColor(agent.agentType)
|
||||||
onBack
|
|
||||||
} = t0;
|
// Handle Esc to go back
|
||||||
const resolvedTools = resolveAgentTools(agent, tools, false);
|
useKeybinding('confirm:no', onBack, { context: 'Confirmation' })
|
||||||
let t1;
|
|
||||||
if ($[0] !== agent) {
|
// Handle Enter to go back
|
||||||
t1 = getActualRelativeAgentFilePath(agent);
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
$[0] = agent;
|
if (e.key === 'return') {
|
||||||
$[1] = t1;
|
e.preventDefault()
|
||||||
} else {
|
onBack()
|
||||||
t1 = $[1];
|
}
|
||||||
}
|
}
|
||||||
const filePath = t1;
|
|
||||||
let t2;
|
function renderToolsList(): React.ReactNode {
|
||||||
if ($[2] !== agent.agentType) {
|
if (resolvedTools.hasWildcard) {
|
||||||
t2 = getAgentColor(agent.agentType);
|
return <Text>All tools</Text>
|
||||||
$[2] = agent.agentType;
|
}
|
||||||
$[3] = t2;
|
|
||||||
} else {
|
if (!agent.tools || agent.tools.length === 0) {
|
||||||
t2 = $[3];
|
return <Text>None</Text>
|
||||||
}
|
}
|
||||||
const backgroundColor = t2;
|
|
||||||
let t3;
|
return (
|
||||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
<>
|
||||||
t3 = {
|
{resolvedTools.validTools.length > 0 && (
|
||||||
context: "Confirmation"
|
<Text>{resolvedTools.validTools.join(', ')}</Text>
|
||||||
};
|
)}
|
||||||
$[4] = t3;
|
{resolvedTools.invalidTools.length > 0 && (
|
||||||
} else {
|
<Text color="warning">
|
||||||
t3 = $[4];
|
{figures.warning} Unrecognized:{' '}
|
||||||
}
|
{resolvedTools.invalidTools.join(', ')}
|
||||||
useKeybinding("confirm:no", onBack, t3);
|
</Text>
|
||||||
let t4;
|
)}
|
||||||
if ($[5] !== onBack) {
|
</>
|
||||||
t4 = e => {
|
)
|
||||||
if (e.key === "return") {
|
}
|
||||||
e.preventDefault();
|
|
||||||
onBack();
|
return (
|
||||||
}
|
<Box
|
||||||
};
|
flexDirection="column"
|
||||||
$[5] = onBack;
|
gap={1}
|
||||||
$[6] = t4;
|
tabIndex={0}
|
||||||
} else {
|
autoFocus
|
||||||
t4 = $[6];
|
onKeyDown={handleKeyDown}
|
||||||
}
|
>
|
||||||
const handleKeyDown = t4;
|
<Text dimColor>{filePath}</Text>
|
||||||
const renderToolsList = function renderToolsList() {
|
|
||||||
if (resolvedTools.hasWildcard) {
|
<Box flexDirection="column">
|
||||||
return <Text>All tools</Text>;
|
<Text>
|
||||||
}
|
<Text bold>Description</Text> (tells Claude when to use this agent):
|
||||||
if (!agent.tools || agent.tools.length === 0) {
|
</Text>
|
||||||
return <Text>None</Text>;
|
<Box marginLeft={2}>
|
||||||
}
|
<Text>{agent.whenToUse}</Text>
|
||||||
return <>{resolvedTools.validTools.length > 0 && <Text>{resolvedTools.validTools.join(", ")}</Text>}{resolvedTools.invalidTools.length > 0 && <Text color="warning">{figures.warning} Unrecognized:{" "}{resolvedTools.invalidTools.join(", ")}</Text>}</>;
|
</Box>
|
||||||
};
|
</Box>
|
||||||
const T0 = Box;
|
|
||||||
const t5 = "column";
|
<Box>
|
||||||
const t6 = 1;
|
<Text>
|
||||||
const t7 = 0;
|
<Text bold>Tools</Text>:{' '}
|
||||||
const t8 = true;
|
</Text>
|
||||||
let t9;
|
{renderToolsList()}
|
||||||
if ($[7] !== filePath) {
|
</Box>
|
||||||
t9 = <Text dimColor={true}>{filePath}</Text>;
|
|
||||||
$[7] = filePath;
|
<Text>
|
||||||
$[8] = t9;
|
<Text bold>Model</Text>: {getAgentModelDisplay(agent.model)}
|
||||||
} else {
|
</Text>
|
||||||
t9 = $[8];
|
|
||||||
}
|
{agent.permissionMode && (
|
||||||
let t10;
|
<Text>
|
||||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
<Text bold>Permission mode</Text>: {agent.permissionMode}
|
||||||
t10 = <Text><Text bold={true}>Description</Text> (tells Claude when to use this agent):</Text>;
|
</Text>
|
||||||
$[9] = t10;
|
)}
|
||||||
} else {
|
|
||||||
t10 = $[9];
|
{agent.memory && (
|
||||||
}
|
<Text>
|
||||||
let t11;
|
<Text bold>Memory</Text>: {getMemoryScopeDisplay(agent.memory)}
|
||||||
if ($[10] !== agent.whenToUse) {
|
</Text>
|
||||||
t11 = <Box flexDirection="column">{t10}<Box marginLeft={2}><Text>{agent.whenToUse}</Text></Box></Box>;
|
)}
|
||||||
$[10] = agent.whenToUse;
|
|
||||||
$[11] = t11;
|
{agent.hooks && Object.keys(agent.hooks).length > 0 && (
|
||||||
} else {
|
<Text>
|
||||||
t11 = $[11];
|
<Text bold>Hooks</Text>: {Object.keys(agent.hooks).join(', ')}
|
||||||
}
|
</Text>
|
||||||
const T1 = Box;
|
)}
|
||||||
let t12;
|
|
||||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
{agent.skills && agent.skills.length > 0 && (
|
||||||
t12 = <Text><Text bold={true}>Tools</Text>:{" "}</Text>;
|
<Text>
|
||||||
$[12] = t12;
|
<Text bold>Skills</Text>:{' '}
|
||||||
} else {
|
{agent.skills.length > 10
|
||||||
t12 = $[12];
|
? `${agent.skills.length} skills`
|
||||||
}
|
: agent.skills.join(', ')}
|
||||||
const t13 = renderToolsList();
|
</Text>
|
||||||
let t14;
|
)}
|
||||||
if ($[13] !== T1 || $[14] !== t12 || $[15] !== t13) {
|
|
||||||
t14 = <T1>{t12}{t13}</T1>;
|
{backgroundColor && (
|
||||||
$[13] = T1;
|
<Box>
|
||||||
$[14] = t12;
|
<Text>
|
||||||
$[15] = t13;
|
<Text bold>Color</Text>:{' '}
|
||||||
$[16] = t14;
|
<Text backgroundColor={backgroundColor} color="inverseText">
|
||||||
} else {
|
{' '}
|
||||||
t14 = $[16];
|
{agent.agentType}{' '}
|
||||||
}
|
</Text>
|
||||||
let t15;
|
</Text>
|
||||||
if ($[17] === Symbol.for("react.memo_cache_sentinel")) {
|
</Box>
|
||||||
t15 = <Text bold={true}>Model</Text>;
|
)}
|
||||||
$[17] = t15;
|
|
||||||
} else {
|
{!isBuiltInAgent(agent) && (
|
||||||
t15 = $[17];
|
<>
|
||||||
}
|
<Box>
|
||||||
let t16;
|
<Text>
|
||||||
if ($[18] !== agent.model) {
|
<Text bold>System prompt</Text>:
|
||||||
t16 = getAgentModelDisplay(agent.model);
|
</Text>
|
||||||
$[18] = agent.model;
|
</Box>
|
||||||
$[19] = t16;
|
<Box marginLeft={2} marginRight={2}>
|
||||||
} else {
|
<Markdown>{agent.getSystemPrompt()}</Markdown>
|
||||||
t16 = $[19];
|
</Box>
|
||||||
}
|
</>
|
||||||
let t17;
|
)}
|
||||||
if ($[20] !== t16) {
|
</Box>
|
||||||
t17 = <Text>{t15}: {t16}</Text>;
|
)
|
||||||
$[20] = t16;
|
|
||||||
$[21] = t17;
|
|
||||||
} else {
|
|
||||||
t17 = $[21];
|
|
||||||
}
|
|
||||||
let t18;
|
|
||||||
if ($[22] !== agent.permissionMode) {
|
|
||||||
t18 = agent.permissionMode && <Text><Text bold={true}>Permission mode</Text>: {agent.permissionMode}</Text>;
|
|
||||||
$[22] = agent.permissionMode;
|
|
||||||
$[23] = t18;
|
|
||||||
} else {
|
|
||||||
t18 = $[23];
|
|
||||||
}
|
|
||||||
let t19;
|
|
||||||
if ($[24] !== agent.memory) {
|
|
||||||
t19 = agent.memory && <Text><Text bold={true}>Memory</Text>: {getMemoryScopeDisplay(agent.memory)}</Text>;
|
|
||||||
$[24] = agent.memory;
|
|
||||||
$[25] = t19;
|
|
||||||
} else {
|
|
||||||
t19 = $[25];
|
|
||||||
}
|
|
||||||
let t20;
|
|
||||||
if ($[26] !== agent.hooks) {
|
|
||||||
t20 = agent.hooks && Object.keys(agent.hooks).length > 0 && <Text><Text bold={true}>Hooks</Text>: {Object.keys(agent.hooks).join(", ")}</Text>;
|
|
||||||
$[26] = agent.hooks;
|
|
||||||
$[27] = t20;
|
|
||||||
} else {
|
|
||||||
t20 = $[27];
|
|
||||||
}
|
|
||||||
let t21;
|
|
||||||
if ($[28] !== agent.skills) {
|
|
||||||
t21 = agent.skills && agent.skills.length > 0 && <Text><Text bold={true}>Skills</Text>:{" "}{agent.skills.length > 10 ? `${agent.skills.length} skills` : agent.skills.join(", ")}</Text>;
|
|
||||||
$[28] = agent.skills;
|
|
||||||
$[29] = t21;
|
|
||||||
} else {
|
|
||||||
t21 = $[29];
|
|
||||||
}
|
|
||||||
let t22;
|
|
||||||
if ($[30] !== agent.agentType || $[31] !== backgroundColor) {
|
|
||||||
t22 = backgroundColor && <Box><Text><Text bold={true}>Color</Text>:{" "}<Text backgroundColor={backgroundColor} color="inverseText">{" "}{agent.agentType}{" "}</Text></Text></Box>;
|
|
||||||
$[30] = agent.agentType;
|
|
||||||
$[31] = backgroundColor;
|
|
||||||
$[32] = t22;
|
|
||||||
} else {
|
|
||||||
t22 = $[32];
|
|
||||||
}
|
|
||||||
let t23;
|
|
||||||
if ($[33] !== agent) {
|
|
||||||
t23 = !isBuiltInAgent(agent) && <><Box><Text><Text bold={true}>System prompt</Text>:</Text></Box><Box marginLeft={2} marginRight={2}><Markdown>{agent.getSystemPrompt()}</Markdown></Box></>;
|
|
||||||
$[33] = agent;
|
|
||||||
$[34] = t23;
|
|
||||||
} else {
|
|
||||||
t23 = $[34];
|
|
||||||
}
|
|
||||||
let t24;
|
|
||||||
if ($[35] !== T0 || $[36] !== handleKeyDown || $[37] !== t11 || $[38] !== t14 || $[39] !== t17 || $[40] !== t18 || $[41] !== t19 || $[42] !== t20 || $[43] !== t21 || $[44] !== t22 || $[45] !== t23 || $[46] !== t9) {
|
|
||||||
t24 = <T0 flexDirection={t5} gap={t6} tabIndex={t7} autoFocus={t8} onKeyDown={handleKeyDown}>{t9}{t11}{t14}{t17}{t18}{t19}{t20}{t21}{t22}{t23}</T0>;
|
|
||||||
$[35] = T0;
|
|
||||||
$[36] = handleKeyDown;
|
|
||||||
$[37] = t11;
|
|
||||||
$[38] = t14;
|
|
||||||
$[39] = t17;
|
|
||||||
$[40] = t18;
|
|
||||||
$[41] = t19;
|
|
||||||
$[42] = t20;
|
|
||||||
$[43] = t21;
|
|
||||||
$[44] = t22;
|
|
||||||
$[45] = t23;
|
|
||||||
$[46] = t9;
|
|
||||||
$[47] = t24;
|
|
||||||
} else {
|
|
||||||
t24 = $[47];
|
|
||||||
}
|
|
||||||
return t24;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,177 +1,246 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk'
|
||||||
import figures from 'figures';
|
import figures from 'figures'
|
||||||
import * as React from 'react';
|
import * as React from 'react'
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import { useSetAppState } from 'src/state/AppState.js';
|
import { useSetAppState } from 'src/state/AppState.js'
|
||||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||||
import type { Tools } from '../../Tool.js';
|
import type { Tools } from '../../Tool.js'
|
||||||
import { type AgentColorName, setAgentColor } from '../../tools/AgentTool/agentColorManager.js';
|
import {
|
||||||
import { type AgentDefinition, getActiveAgentsFromList, isCustomAgent, isPluginAgent } from '../../tools/AgentTool/loadAgentsDir.js';
|
type AgentColorName,
|
||||||
import { editFileInEditor } from '../../utils/promptEditor.js';
|
setAgentColor,
|
||||||
import { getActualAgentFilePath, updateAgentFile } from './agentFileUtils.js';
|
} from '../../tools/AgentTool/agentColorManager.js'
|
||||||
import { ColorPicker } from './ColorPicker.js';
|
import {
|
||||||
import { ModelSelector } from './ModelSelector.js';
|
type AgentDefinition,
|
||||||
import { ToolSelector } from './ToolSelector.js';
|
getActiveAgentsFromList,
|
||||||
import { getAgentSourceDisplayName } from './utils.js';
|
isCustomAgent,
|
||||||
|
isPluginAgent,
|
||||||
|
} from '../../tools/AgentTool/loadAgentsDir.js'
|
||||||
|
import { editFileInEditor } from '../../utils/promptEditor.js'
|
||||||
|
import { getActualAgentFilePath, updateAgentFile } from './agentFileUtils.js'
|
||||||
|
import { ColorPicker } from './ColorPicker.js'
|
||||||
|
import { ModelSelector } from './ModelSelector.js'
|
||||||
|
import { ToolSelector } from './ToolSelector.js'
|
||||||
|
import { getAgentSourceDisplayName } from './utils.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
agent: AgentDefinition;
|
agent: AgentDefinition
|
||||||
tools: Tools;
|
tools: Tools
|
||||||
onSaved: (message: string) => void;
|
onSaved: (message: string) => void
|
||||||
onBack: () => void;
|
onBack: () => void
|
||||||
};
|
}
|
||||||
type EditMode = 'menu' | 'edit-tools' | 'edit-color' | 'edit-model';
|
|
||||||
|
type EditMode = 'menu' | 'edit-tools' | 'edit-color' | 'edit-model'
|
||||||
|
|
||||||
type SaveChanges = {
|
type SaveChanges = {
|
||||||
tools?: string[];
|
tools?: string[]
|
||||||
color?: AgentColorName;
|
color?: AgentColorName
|
||||||
model?: string;
|
model?: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export function AgentEditor({
|
export function AgentEditor({
|
||||||
agent,
|
agent,
|
||||||
tools,
|
tools,
|
||||||
onSaved,
|
onSaved,
|
||||||
onBack
|
onBack,
|
||||||
}: Props): React.ReactNode {
|
}: Props): React.ReactNode {
|
||||||
const setAppState = useSetAppState();
|
const setAppState = useSetAppState()
|
||||||
const [editMode, setEditMode] = useState<EditMode>('menu');
|
const [editMode, setEditMode] = useState<EditMode>('menu')
|
||||||
const [selectedMenuIndex, setSelectedMenuIndex] = useState(0);
|
const [selectedMenuIndex, setSelectedMenuIndex] = useState(0)
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [selectedColor, setSelectedColor] = useState<AgentColorName | undefined>(agent.color as AgentColorName | undefined);
|
const [selectedColor, setSelectedColor] = useState<
|
||||||
|
AgentColorName | undefined
|
||||||
|
>(agent.color as AgentColorName | undefined)
|
||||||
|
|
||||||
const handleOpenInEditor = useCallback(async () => {
|
const handleOpenInEditor = useCallback(async () => {
|
||||||
const filePath = getActualAgentFilePath(agent);
|
const filePath = getActualAgentFilePath(agent)
|
||||||
const result = await editFileInEditor(filePath);
|
const result = await editFileInEditor(filePath)
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
setError(result.error);
|
setError(result.error)
|
||||||
} else {
|
} else {
|
||||||
onSaved(`Opened ${agent.agentType} in editor. If you made edits, restart to load the latest version.`);
|
onSaved(
|
||||||
|
`Opened ${agent.agentType} in editor. If you made edits, restart to load the latest version.`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [agent, onSaved]);
|
}, [agent, onSaved])
|
||||||
const handleSave = useCallback(async (changes: SaveChanges = {}) => {
|
|
||||||
const {
|
const handleSave = useCallback(
|
||||||
tools: newTools,
|
async (changes: SaveChanges = {}) => {
|
||||||
color: newColor,
|
const { tools: newTools, color: newColor, model: newModel } = changes
|
||||||
model: newModel
|
const finalColor = newColor ?? selectedColor
|
||||||
} = changes;
|
const hasToolsChanged = newTools !== undefined
|
||||||
const finalColor = newColor ?? selectedColor;
|
const hasModelChanged = newModel !== undefined
|
||||||
const hasToolsChanged = newTools !== undefined;
|
const hasColorChanged = finalColor !== agent.color
|
||||||
const hasModelChanged = newModel !== undefined;
|
|
||||||
const hasColorChanged = finalColor !== agent.color;
|
if (!hasToolsChanged && !hasModelChanged && !hasColorChanged) {
|
||||||
if (!hasToolsChanged && !hasModelChanged && !hasColorChanged) {
|
return false
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Only custom/plugin agents can be edited
|
|
||||||
// this is for type safety; the UI shouldn't allow editing otherwise
|
|
||||||
if (!isCustomAgent(agent) && !isPluginAgent(agent)) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
await updateAgentFile(agent, agent.whenToUse, newTools ?? agent.tools, agent.getSystemPrompt(), finalColor, newModel ?? agent.model);
|
|
||||||
if (hasColorChanged && finalColor) {
|
try {
|
||||||
setAgentColor(agent.agentType, finalColor);
|
// Only custom/plugin agents can be edited
|
||||||
}
|
// this is for type safety; the UI shouldn't allow editing otherwise
|
||||||
setAppState(state => {
|
if (!isCustomAgent(agent) && !isPluginAgent(agent)) {
|
||||||
const allAgents = state.agentDefinitions.allAgents.map(a => a.agentType === agent.agentType ? {
|
return false
|
||||||
...a,
|
}
|
||||||
tools: newTools ?? a.tools,
|
|
||||||
color: finalColor,
|
await updateAgentFile(
|
||||||
model: newModel ?? a.model
|
agent,
|
||||||
} : a);
|
agent.whenToUse,
|
||||||
return {
|
newTools ?? agent.tools,
|
||||||
...state,
|
agent.getSystemPrompt(),
|
||||||
agentDefinitions: {
|
finalColor,
|
||||||
...state.agentDefinitions,
|
newModel ?? agent.model,
|
||||||
activeAgents: getActiveAgentsFromList(allAgents),
|
)
|
||||||
allAgents
|
|
||||||
|
if (hasColorChanged && finalColor) {
|
||||||
|
setAgentColor(agent.agentType, finalColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppState(state => {
|
||||||
|
const allAgents = state.agentDefinitions.allAgents.map(a =>
|
||||||
|
a.agentType === agent.agentType
|
||||||
|
? {
|
||||||
|
...a,
|
||||||
|
tools: newTools ?? a.tools,
|
||||||
|
color: finalColor,
|
||||||
|
model: newModel ?? a.model,
|
||||||
|
}
|
||||||
|
: a,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
agentDefinitions: {
|
||||||
|
...state.agentDefinitions,
|
||||||
|
activeAgents: getActiveAgentsFromList(allAgents),
|
||||||
|
allAgents,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
})
|
||||||
});
|
|
||||||
onSaved(`Updated agent: ${chalk.bold(agent.agentType)}`);
|
onSaved(`Updated agent: ${chalk.bold(agent.agentType)}`)
|
||||||
return true;
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to save agent');
|
setError(err instanceof Error ? err.message : 'Failed to save agent')
|
||||||
return false;
|
return false
|
||||||
}
|
|
||||||
}, [agent, selectedColor, onSaved, setAppState]);
|
|
||||||
const menuItems = useMemo(() => [{
|
|
||||||
label: 'Open in editor',
|
|
||||||
action: handleOpenInEditor
|
|
||||||
}, {
|
|
||||||
label: 'Edit tools',
|
|
||||||
action: () => setEditMode('edit-tools')
|
|
||||||
}, {
|
|
||||||
label: 'Edit model',
|
|
||||||
action: () => setEditMode('edit-model')
|
|
||||||
}, {
|
|
||||||
label: 'Edit color',
|
|
||||||
action: () => setEditMode('edit-color')
|
|
||||||
}], [handleOpenInEditor]);
|
|
||||||
const handleEscape = useCallback(() => {
|
|
||||||
setError(null);
|
|
||||||
if (editMode === 'menu') {
|
|
||||||
onBack();
|
|
||||||
} else {
|
|
||||||
setEditMode('menu');
|
|
||||||
}
|
|
||||||
}, [editMode, onBack]);
|
|
||||||
const handleMenuKeyDown = useCallback((e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'up') {
|
|
||||||
e.preventDefault();
|
|
||||||
setSelectedMenuIndex(index => Math.max(0, index - 1));
|
|
||||||
} else if (e.key === 'down') {
|
|
||||||
e.preventDefault();
|
|
||||||
setSelectedMenuIndex(index_0 => Math.min(menuItems.length - 1, index_0 + 1));
|
|
||||||
} else if (e.key === 'return') {
|
|
||||||
e.preventDefault();
|
|
||||||
const selectedItem = menuItems[selectedMenuIndex];
|
|
||||||
if (selectedItem) {
|
|
||||||
void selectedItem.action();
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[agent, selectedColor, onSaved, setAppState],
|
||||||
|
)
|
||||||
|
|
||||||
|
const menuItems = useMemo(
|
||||||
|
() => [
|
||||||
|
{ label: 'Open in editor', action: handleOpenInEditor },
|
||||||
|
{ label: 'Edit tools', action: () => setEditMode('edit-tools') },
|
||||||
|
{ label: 'Edit model', action: () => setEditMode('edit-model') },
|
||||||
|
{ label: 'Edit color', action: () => setEditMode('edit-color') },
|
||||||
|
],
|
||||||
|
[handleOpenInEditor],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleEscape = useCallback(() => {
|
||||||
|
setError(null)
|
||||||
|
if (editMode === 'menu') {
|
||||||
|
onBack()
|
||||||
|
} else {
|
||||||
|
setEditMode('menu')
|
||||||
}
|
}
|
||||||
}, [menuItems, selectedMenuIndex]);
|
}, [editMode, onBack])
|
||||||
useKeybinding('confirm:no', handleEscape, {
|
|
||||||
context: 'Confirmation'
|
const handleMenuKeyDown = useCallback(
|
||||||
});
|
(e: KeyboardEvent) => {
|
||||||
const renderMenu = (): React.ReactNode => <Box flexDirection="column" tabIndex={0} autoFocus onKeyDown={handleMenuKeyDown}>
|
if (e.key === 'up') {
|
||||||
|
e.preventDefault()
|
||||||
|
setSelectedMenuIndex(index => Math.max(0, index - 1))
|
||||||
|
} else if (e.key === 'down') {
|
||||||
|
e.preventDefault()
|
||||||
|
setSelectedMenuIndex(index => Math.min(menuItems.length - 1, index + 1))
|
||||||
|
} else if (e.key === 'return') {
|
||||||
|
e.preventDefault()
|
||||||
|
const selectedItem = menuItems[selectedMenuIndex]
|
||||||
|
if (selectedItem) {
|
||||||
|
void selectedItem.action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[menuItems, selectedMenuIndex],
|
||||||
|
)
|
||||||
|
|
||||||
|
useKeybinding('confirm:no', handleEscape, { context: 'Confirmation' })
|
||||||
|
|
||||||
|
const renderMenu = (): React.ReactNode => (
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
tabIndex={0}
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={handleMenuKeyDown}
|
||||||
|
>
|
||||||
<Text dimColor>Source: {getAgentSourceDisplayName(agent.source)}</Text>
|
<Text dimColor>Source: {getAgentSourceDisplayName(agent.source)}</Text>
|
||||||
|
|
||||||
<Box marginTop={1} flexDirection="column">
|
<Box marginTop={1} flexDirection="column">
|
||||||
{menuItems.map((item, index_1) => <Text key={item.label} color={index_1 === selectedMenuIndex ? 'suggestion' : undefined}>
|
{menuItems.map((item, index) => (
|
||||||
{index_1 === selectedMenuIndex ? `${figures.pointer} ` : ' '}
|
<Text
|
||||||
|
key={item.label}
|
||||||
|
color={index === selectedMenuIndex ? 'suggestion' : undefined}
|
||||||
|
>
|
||||||
|
{index === selectedMenuIndex ? `${figures.pointer} ` : ' '}
|
||||||
{item.label}
|
{item.label}
|
||||||
</Text>)}
|
</Text>
|
||||||
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{error && <Box marginTop={1}>
|
{error && (
|
||||||
|
<Box marginTop={1}>
|
||||||
<Text color="error">{error}</Text>
|
<Text color="error">{error}</Text>
|
||||||
</Box>}
|
</Box>
|
||||||
</Box>;
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
|
||||||
switch (editMode) {
|
switch (editMode) {
|
||||||
case 'menu':
|
case 'menu':
|
||||||
return renderMenu();
|
return renderMenu()
|
||||||
|
|
||||||
case 'edit-tools':
|
case 'edit-tools':
|
||||||
return <ToolSelector tools={tools} initialTools={agent.tools} onComplete={async finalTools => {
|
return (
|
||||||
setEditMode('menu');
|
<ToolSelector
|
||||||
await handleSave({
|
tools={tools}
|
||||||
tools: finalTools
|
initialTools={agent.tools}
|
||||||
});
|
onComplete={async finalTools => {
|
||||||
}} />;
|
setEditMode('menu')
|
||||||
|
await handleSave({ tools: finalTools })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
case 'edit-color':
|
case 'edit-color':
|
||||||
return <ColorPicker agentName={agent.agentType} currentColor={selectedColor || agent.color as AgentColorName || 'automatic'} onConfirm={async color => {
|
return (
|
||||||
setSelectedColor(color);
|
<ColorPicker
|
||||||
setEditMode('menu');
|
agentName={agent.agentType}
|
||||||
await handleSave({
|
currentColor={
|
||||||
color
|
selectedColor || (agent.color as AgentColorName) || 'automatic'
|
||||||
});
|
}
|
||||||
}} />;
|
onConfirm={async color => {
|
||||||
|
setSelectedColor(color)
|
||||||
|
setEditMode('menu')
|
||||||
|
await handleSave({ color })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
case 'edit-model':
|
case 'edit-model':
|
||||||
return <ModelSelector initialModel={agent.model} onComplete={async model => {
|
return (
|
||||||
setEditMode('menu');
|
<ModelSelector
|
||||||
await handleSave({
|
initialModel={agent.model}
|
||||||
model
|
onComplete={async model => {
|
||||||
});
|
setEditMode('menu')
|
||||||
}} />;
|
await handleSave({ model })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
|
||||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
instructions?: string;
|
instructions?: string
|
||||||
};
|
}
|
||||||
export function AgentNavigationFooter(t0) {
|
|
||||||
const $ = _c(2);
|
export function AgentNavigationFooter({
|
||||||
const {
|
instructions = 'Press ↑↓ to navigate · Enter to select · Esc to go back',
|
||||||
instructions: t1
|
}: Props): React.ReactNode {
|
||||||
} = t0;
|
const exitState = useExitOnCtrlCDWithKeybindings()
|
||||||
const instructions = t1 === undefined ? "Press \u2191\u2193 to navigate \xB7 Enter to select \xB7 Esc to go back" : t1;
|
|
||||||
const exitState = useExitOnCtrlCDWithKeybindings();
|
return (
|
||||||
const t2 = exitState.pending ? `Press ${exitState.keyName} again to exit` : instructions;
|
<Box marginLeft={2}>
|
||||||
let t3;
|
<Text dimColor>
|
||||||
if ($[0] !== t2) {
|
{exitState.pending
|
||||||
t3 = <Box marginLeft={2}><Text dimColor={true}>{t2}</Text></Box>;
|
? `Press ${exitState.keyName} again to exit`
|
||||||
$[0] = t2;
|
: instructions}
|
||||||
$[1] = t3;
|
</Text>
|
||||||
} else {
|
</Box>
|
||||||
t3 = $[1];
|
)
|
||||||
}
|
|
||||||
return t3;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,439 +1,342 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import figures from 'figures'
|
||||||
import figures from 'figures';
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import type { SettingSource } from 'src/utils/settings/constants.js'
|
||||||
import type { SettingSource } from 'src/utils/settings/constants.js';
|
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import type { ResolvedAgent } from '../../tools/AgentTool/agentDisplay.js'
|
||||||
import type { ResolvedAgent } from '../../tools/AgentTool/agentDisplay.js';
|
import {
|
||||||
import { AGENT_SOURCE_GROUPS, compareAgentsByName, getOverrideSourceLabel, resolveAgentModelDisplay } from '../../tools/AgentTool/agentDisplay.js';
|
AGENT_SOURCE_GROUPS,
|
||||||
import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js';
|
compareAgentsByName,
|
||||||
import { count } from '../../utils/array.js';
|
getOverrideSourceLabel,
|
||||||
import { Dialog } from '../design-system/Dialog.js';
|
resolveAgentModelDisplay,
|
||||||
import { Divider } from '../design-system/Divider.js';
|
} from '../../tools/AgentTool/agentDisplay.js'
|
||||||
import { getAgentSourceDisplayName } from './utils.js';
|
import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js'
|
||||||
|
import { count } from '../../utils/array.js'
|
||||||
|
import { Dialog } from '../design-system/Dialog.js'
|
||||||
|
import { Divider } from '../design-system/Divider.js'
|
||||||
|
import { getAgentSourceDisplayName } from './utils.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
source: SettingSource | 'all' | 'built-in' | 'plugin';
|
source: SettingSource | 'all' | 'built-in' | 'plugin'
|
||||||
agents: ResolvedAgent[];
|
agents: ResolvedAgent[]
|
||||||
onBack: () => void;
|
onBack: () => void
|
||||||
onSelect: (agent: AgentDefinition) => void;
|
onSelect: (agent: AgentDefinition) => void
|
||||||
onCreateNew?: () => void;
|
onCreateNew?: () => void
|
||||||
changes?: string[];
|
changes?: string[]
|
||||||
};
|
}
|
||||||
export function AgentsList(t0) {
|
|
||||||
const $ = _c(96);
|
export function AgentsList({
|
||||||
const {
|
source,
|
||||||
source,
|
agents,
|
||||||
agents,
|
onBack,
|
||||||
onBack,
|
onSelect,
|
||||||
onSelect,
|
onCreateNew,
|
||||||
onCreateNew,
|
changes,
|
||||||
changes
|
}: Props): React.ReactNode {
|
||||||
} = t0;
|
const [selectedAgent, setSelectedAgent] =
|
||||||
const [selectedAgent, setSelectedAgent] = React.useState(null);
|
React.useState<ResolvedAgent | null>(null)
|
||||||
const [isCreateNewSelected, setIsCreateNewSelected] = React.useState(true);
|
const [isCreateNewSelected, setIsCreateNewSelected] = React.useState(true)
|
||||||
let t1;
|
|
||||||
if ($[0] !== agents) {
|
// Sort agents alphabetically by name within each source group
|
||||||
t1 = [...agents].sort(compareAgentsByName);
|
const sortedAgents = React.useMemo(
|
||||||
$[0] = agents;
|
() => [...agents].sort(compareAgentsByName),
|
||||||
$[1] = t1;
|
[agents],
|
||||||
} else {
|
)
|
||||||
t1 = $[1];
|
|
||||||
}
|
const getOverrideInfo = (agent: ResolvedAgent) => {
|
||||||
const sortedAgents = t1;
|
return {
|
||||||
const getOverrideInfo = _temp;
|
isOverridden: !!agent.overriddenBy,
|
||||||
let t2;
|
overriddenBy: agent.overriddenBy || null,
|
||||||
if ($[2] !== isCreateNewSelected) {
|
|
||||||
t2 = () => <Box><Text color={isCreateNewSelected ? "suggestion" : undefined}>{isCreateNewSelected ? `${figures.pointer} ` : " "}</Text><Text color={isCreateNewSelected ? "suggestion" : undefined}>Create new agent</Text></Box>;
|
|
||||||
$[2] = isCreateNewSelected;
|
|
||||||
$[3] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[3];
|
|
||||||
}
|
|
||||||
const renderCreateNewOption = t2;
|
|
||||||
let t3;
|
|
||||||
if ($[4] !== isCreateNewSelected || $[5] !== selectedAgent?.agentType || $[6] !== selectedAgent?.source) {
|
|
||||||
t3 = agent_0 => {
|
|
||||||
const isBuiltIn = agent_0.source === "built-in";
|
|
||||||
const isSelected = !isBuiltIn && !isCreateNewSelected && selectedAgent?.agentType === agent_0.agentType && selectedAgent?.source === agent_0.source;
|
|
||||||
const {
|
|
||||||
isOverridden,
|
|
||||||
overriddenBy
|
|
||||||
} = getOverrideInfo(agent_0);
|
|
||||||
const dimmed = isBuiltIn || isOverridden;
|
|
||||||
const textColor = !isBuiltIn && isSelected ? "suggestion" : undefined;
|
|
||||||
const resolvedModel = resolveAgentModelDisplay(agent_0);
|
|
||||||
return <Box key={`${agent_0.agentType}-${agent_0.source}`}><Text dimColor={dimmed && !isSelected} color={textColor}>{isBuiltIn ? "" : isSelected ? `${figures.pointer} ` : " "}</Text><Text dimColor={dimmed && !isSelected} color={textColor}>{agent_0.agentType}</Text>{resolvedModel && <Text dimColor={true} color={textColor}>{" \xB7 "}{resolvedModel}</Text>}{agent_0.memory && <Text dimColor={true} color={textColor}>{" \xB7 "}{agent_0.memory} memory</Text>}{overriddenBy && <Text dimColor={!isSelected} color={isSelected ? "warning" : undefined}>{" "}{figures.warning} shadowed by {getOverrideSourceLabel(overriddenBy)}</Text>}</Box>;
|
|
||||||
};
|
|
||||||
$[4] = isCreateNewSelected;
|
|
||||||
$[5] = selectedAgent?.agentType;
|
|
||||||
$[6] = selectedAgent?.source;
|
|
||||||
$[7] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[7];
|
|
||||||
}
|
|
||||||
const renderAgent = t3;
|
|
||||||
let t4;
|
|
||||||
if ($[8] !== sortedAgents || $[9] !== source) {
|
|
||||||
bb0: {
|
|
||||||
const nonBuiltIn = sortedAgents.filter(_temp2);
|
|
||||||
if (source === "all") {
|
|
||||||
t4 = AGENT_SOURCE_GROUPS.filter(_temp3).flatMap(t5 => {
|
|
||||||
const {
|
|
||||||
source: groupSource
|
|
||||||
} = t5;
|
|
||||||
return nonBuiltIn.filter(a_0 => a_0.source === groupSource);
|
|
||||||
});
|
|
||||||
break bb0;
|
|
||||||
}
|
|
||||||
t4 = nonBuiltIn;
|
|
||||||
}
|
}
|
||||||
$[8] = sortedAgents;
|
|
||||||
$[9] = source;
|
|
||||||
$[10] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[10];
|
|
||||||
}
|
}
|
||||||
const selectableAgentsInOrder = t4;
|
|
||||||
let t5;
|
const renderCreateNewOption = () => {
|
||||||
let t6;
|
return (
|
||||||
if ($[11] !== isCreateNewSelected || $[12] !== onCreateNew || $[13] !== selectableAgentsInOrder || $[14] !== selectedAgent) {
|
<Box>
|
||||||
t5 = () => {
|
<Text color={isCreateNewSelected ? 'suggestion' : undefined}>
|
||||||
if (!selectedAgent && !isCreateNewSelected && selectableAgentsInOrder.length > 0) {
|
{isCreateNewSelected ? `${figures.pointer} ` : ' '}
|
||||||
if (onCreateNew) {
|
</Text>
|
||||||
setIsCreateNewSelected(true);
|
<Text color={isCreateNewSelected ? 'suggestion' : undefined}>
|
||||||
} else {
|
Create new agent
|
||||||
setSelectedAgent(selectableAgentsInOrder[0] || null);
|
</Text>
|
||||||
}
|
</Box>
|
||||||
}
|
)
|
||||||
};
|
|
||||||
t6 = [selectableAgentsInOrder, selectedAgent, isCreateNewSelected, onCreateNew];
|
|
||||||
$[11] = isCreateNewSelected;
|
|
||||||
$[12] = onCreateNew;
|
|
||||||
$[13] = selectableAgentsInOrder;
|
|
||||||
$[14] = selectedAgent;
|
|
||||||
$[15] = t5;
|
|
||||||
$[16] = t6;
|
|
||||||
} else {
|
|
||||||
t5 = $[15];
|
|
||||||
t6 = $[16];
|
|
||||||
}
|
}
|
||||||
React.useEffect(t5, t6);
|
|
||||||
let t7;
|
const renderAgent = (agent: ResolvedAgent) => {
|
||||||
if ($[17] !== isCreateNewSelected || $[18] !== onCreateNew || $[19] !== onSelect || $[20] !== selectableAgentsInOrder || $[21] !== selectedAgent) {
|
const isBuiltIn = agent.source === 'built-in'
|
||||||
t7 = e => {
|
const isSelected =
|
||||||
if (e.key === "return") {
|
!isBuiltIn &&
|
||||||
e.preventDefault();
|
!isCreateNewSelected &&
|
||||||
if (isCreateNewSelected && onCreateNew) {
|
selectedAgent?.agentType === agent.agentType &&
|
||||||
onCreateNew();
|
selectedAgent?.source === agent.source
|
||||||
} else {
|
|
||||||
if (selectedAgent) {
|
const { isOverridden, overriddenBy } = getOverrideInfo(agent)
|
||||||
onSelect(selectedAgent);
|
const dimmed = isBuiltIn || isOverridden
|
||||||
}
|
const textColor = !isBuiltIn && isSelected ? 'suggestion' : undefined
|
||||||
}
|
|
||||||
return;
|
const resolvedModel = resolveAgentModelDisplay(agent)
|
||||||
}
|
|
||||||
if (e.key !== "up" && e.key !== "down") {
|
return (
|
||||||
return;
|
<Box key={`${agent.agentType}-${agent.source}`}>
|
||||||
}
|
<Text dimColor={dimmed && !isSelected} color={textColor}>
|
||||||
e.preventDefault();
|
{isBuiltIn ? '' : isSelected ? `${figures.pointer} ` : ' '}
|
||||||
const hasCreateOption = !!onCreateNew;
|
</Text>
|
||||||
const totalItems = selectableAgentsInOrder.length + (hasCreateOption ? 1 : 0);
|
<Text dimColor={dimmed && !isSelected} color={textColor}>
|
||||||
if (totalItems === 0) {
|
{agent.agentType}
|
||||||
return;
|
</Text>
|
||||||
}
|
{resolvedModel && (
|
||||||
let currentPosition = 0;
|
<Text dimColor={true} color={textColor}>
|
||||||
if (!isCreateNewSelected && selectedAgent) {
|
{' · '}
|
||||||
const agentIndex = selectableAgentsInOrder.findIndex(a_1 => a_1.agentType === selectedAgent.agentType && a_1.source === selectedAgent.source);
|
{resolvedModel}
|
||||||
if (agentIndex >= 0) {
|
</Text>
|
||||||
currentPosition = hasCreateOption ? agentIndex + 1 : agentIndex;
|
)}
|
||||||
}
|
{agent.memory && (
|
||||||
}
|
<Text dimColor={true} color={textColor}>
|
||||||
const newPosition = e.key === "up" ? currentPosition === 0 ? totalItems - 1 : currentPosition - 1 : currentPosition === totalItems - 1 ? 0 : currentPosition + 1;
|
{' · '}
|
||||||
if (hasCreateOption && newPosition === 0) {
|
{agent.memory} memory
|
||||||
setIsCreateNewSelected(true);
|
</Text>
|
||||||
setSelectedAgent(null);
|
)}
|
||||||
} else {
|
{overriddenBy && (
|
||||||
const agentIndex_0 = hasCreateOption ? newPosition - 1 : newPosition;
|
<Text
|
||||||
const newAgent = selectableAgentsInOrder[agentIndex_0];
|
dimColor={!isSelected}
|
||||||
if (newAgent) {
|
color={isSelected ? 'warning' : undefined}
|
||||||
setIsCreateNewSelected(false);
|
>
|
||||||
setSelectedAgent(newAgent);
|
{' '}
|
||||||
}
|
{figures.warning} shadowed by {getOverrideSourceLabel(overriddenBy)}
|
||||||
}
|
</Text>
|
||||||
};
|
)}
|
||||||
$[17] = isCreateNewSelected;
|
</Box>
|
||||||
$[18] = onCreateNew;
|
)
|
||||||
$[19] = onSelect;
|
|
||||||
$[20] = selectableAgentsInOrder;
|
|
||||||
$[21] = selectedAgent;
|
|
||||||
$[22] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[22];
|
|
||||||
}
|
}
|
||||||
const handleKeyDown = t7;
|
|
||||||
let t8;
|
const selectableAgentsInOrder = React.useMemo(() => {
|
||||||
if ($[23] !== renderAgent || $[24] !== sortedAgents) {
|
const nonBuiltIn = sortedAgents.filter(a => a.source !== 'built-in')
|
||||||
t8 = t9 => {
|
if (source === 'all') {
|
||||||
const title = t9 === undefined ? "Built-in (always available):" : t9;
|
return AGENT_SOURCE_GROUPS.filter(g => g.source !== 'built-in').flatMap(
|
||||||
const builtInAgents = sortedAgents.filter(_temp4);
|
({ source: groupSource }) =>
|
||||||
return <Box flexDirection="column" marginBottom={1} paddingLeft={2}><Text bold={true} dimColor={true}>{title}</Text>{builtInAgents.map(renderAgent)}</Box>;
|
nonBuiltIn.filter(a => a.source === groupSource),
|
||||||
};
|
)
|
||||||
$[23] = renderAgent;
|
}
|
||||||
$[24] = sortedAgents;
|
return nonBuiltIn
|
||||||
$[25] = t8;
|
}, [sortedAgents, source])
|
||||||
} else {
|
|
||||||
t8 = $[25];
|
// Set initial selection
|
||||||
}
|
React.useEffect(() => {
|
||||||
const renderBuiltInAgentsSection = t8;
|
if (
|
||||||
let t9;
|
!selectedAgent &&
|
||||||
if ($[26] !== renderAgent) {
|
!isCreateNewSelected &&
|
||||||
t9 = (title_0, groupAgents) => {
|
selectableAgentsInOrder.length > 0
|
||||||
if (!groupAgents.length) {
|
) {
|
||||||
return null;
|
if (onCreateNew) {
|
||||||
}
|
setIsCreateNewSelected(true)
|
||||||
const folderPath = groupAgents[0]?.baseDir;
|
} else {
|
||||||
return <Box flexDirection="column" marginBottom={1}><Box paddingLeft={2}><Text bold={true} dimColor={true}>{title_0}</Text>{folderPath && <Text dimColor={true}> ({folderPath})</Text>}</Box>{groupAgents.map(agent_1 => renderAgent(agent_1))}</Box>;
|
setSelectedAgent(selectableAgentsInOrder[0] || null)
|
||||||
};
|
}
|
||||||
$[26] = renderAgent;
|
}
|
||||||
$[27] = t9;
|
}, [selectableAgentsInOrder, selectedAgent, isCreateNewSelected, onCreateNew])
|
||||||
} else {
|
|
||||||
t9 = $[27];
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
}
|
if (e.key === 'return') {
|
||||||
const renderAgentGroup = t9;
|
e.preventDefault()
|
||||||
let t10;
|
if (isCreateNewSelected && onCreateNew) {
|
||||||
if ($[28] !== source) {
|
onCreateNew()
|
||||||
t10 = getAgentSourceDisplayName(source);
|
} else if (selectedAgent) {
|
||||||
$[28] = source;
|
onSelect(selectedAgent)
|
||||||
$[29] = t10;
|
}
|
||||||
} else {
|
return
|
||||||
t10 = $[29];
|
}
|
||||||
}
|
|
||||||
const sourceTitle = t10;
|
if (e.key !== 'up' && e.key !== 'down') return
|
||||||
let T0;
|
e.preventDefault()
|
||||||
let T1;
|
|
||||||
let t11;
|
// Handle navigation with "Create New Agent" option
|
||||||
let t12;
|
const hasCreateOption = !!onCreateNew
|
||||||
let t13;
|
const totalItems =
|
||||||
let t14;
|
selectableAgentsInOrder.length + (hasCreateOption ? 1 : 0)
|
||||||
let t15;
|
|
||||||
let t16;
|
if (totalItems === 0) return
|
||||||
let t17;
|
|
||||||
let t18;
|
// Calculate current position in list (0 = create new, 1+ = agents)
|
||||||
let t19;
|
let currentPosition = 0
|
||||||
let t20;
|
if (!isCreateNewSelected && selectedAgent) {
|
||||||
let t21;
|
const agentIndex = selectableAgentsInOrder.findIndex(
|
||||||
let t22;
|
a =>
|
||||||
if ($[30] !== changes || $[31] !== handleKeyDown || $[32] !== onBack || $[33] !== onCreateNew || $[34] !== renderAgent || $[35] !== renderAgentGroup || $[36] !== renderBuiltInAgentsSection || $[37] !== renderCreateNewOption || $[38] !== sortedAgents || $[39] !== source || $[40] !== sourceTitle) {
|
a.agentType === selectedAgent.agentType &&
|
||||||
t22 = Symbol.for("react.early_return_sentinel");
|
a.source === selectedAgent.source,
|
||||||
bb1: {
|
)
|
||||||
const builtInAgents_0 = sortedAgents.filter(_temp5);
|
if (agentIndex >= 0) {
|
||||||
const hasNoAgents = !sortedAgents.length || source !== "built-in" && !sortedAgents.some(_temp6);
|
currentPosition = hasCreateOption ? agentIndex + 1 : agentIndex
|
||||||
if (hasNoAgents) {
|
}
|
||||||
let t23;
|
}
|
||||||
if ($[55] !== onCreateNew || $[56] !== renderCreateNewOption) {
|
|
||||||
t23 = onCreateNew && <Box>{renderCreateNewOption()}</Box>;
|
// Calculate new position with wrap-around
|
||||||
$[55] = onCreateNew;
|
const newPosition =
|
||||||
$[56] = renderCreateNewOption;
|
e.key === 'up'
|
||||||
$[57] = t23;
|
? currentPosition === 0
|
||||||
} else {
|
? totalItems - 1
|
||||||
t23 = $[57];
|
: currentPosition - 1
|
||||||
}
|
: currentPosition === totalItems - 1
|
||||||
let t24;
|
? 0
|
||||||
let t25;
|
: currentPosition + 1
|
||||||
let t26;
|
|
||||||
if ($[58] === Symbol.for("react.memo_cache_sentinel")) {
|
// Update selection based on new position
|
||||||
t24 = <Text dimColor={true}>No agents found. Create specialized subagents that Claude can delegate to.</Text>;
|
if (hasCreateOption && newPosition === 0) {
|
||||||
t25 = <Text dimColor={true}>Each subagent has its own context window, custom system prompt, and specific tools.</Text>;
|
setIsCreateNewSelected(true)
|
||||||
t26 = <Text dimColor={true}>Try creating: Code Reviewer, Code Simplifier, Security Reviewer, Tech Lead, or UX Reviewer.</Text>;
|
setSelectedAgent(null)
|
||||||
$[58] = t24;
|
} else {
|
||||||
$[59] = t25;
|
const agentIndex = hasCreateOption ? newPosition - 1 : newPosition
|
||||||
$[60] = t26;
|
const newAgent = selectableAgentsInOrder[agentIndex]
|
||||||
} else {
|
if (newAgent) {
|
||||||
t24 = $[58];
|
setIsCreateNewSelected(false)
|
||||||
t25 = $[59];
|
setSelectedAgent(newAgent)
|
||||||
t26 = $[60];
|
}
|
||||||
}
|
|
||||||
let t27;
|
|
||||||
if ($[61] !== renderBuiltInAgentsSection || $[62] !== sortedAgents || $[63] !== source) {
|
|
||||||
t27 = source !== "built-in" && sortedAgents.some(_temp7) && <><Divider />{renderBuiltInAgentsSection()}</>;
|
|
||||||
$[61] = renderBuiltInAgentsSection;
|
|
||||||
$[62] = sortedAgents;
|
|
||||||
$[63] = source;
|
|
||||||
$[64] = t27;
|
|
||||||
} else {
|
|
||||||
t27 = $[64];
|
|
||||||
}
|
|
||||||
let t28;
|
|
||||||
if ($[65] !== handleKeyDown || $[66] !== t23 || $[67] !== t27) {
|
|
||||||
t28 = <Box flexDirection="column" gap={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t23}{t24}{t25}{t26}{t27}</Box>;
|
|
||||||
$[65] = handleKeyDown;
|
|
||||||
$[66] = t23;
|
|
||||||
$[67] = t27;
|
|
||||||
$[68] = t28;
|
|
||||||
} else {
|
|
||||||
t28 = $[68];
|
|
||||||
}
|
|
||||||
let t29;
|
|
||||||
if ($[69] !== onBack || $[70] !== sourceTitle || $[71] !== t28) {
|
|
||||||
t29 = <Dialog title={sourceTitle} subtitle="No agents found" onCancel={onBack} hideInputGuide={true}>{t28}</Dialog>;
|
|
||||||
$[69] = onBack;
|
|
||||||
$[70] = sourceTitle;
|
|
||||||
$[71] = t28;
|
|
||||||
$[72] = t29;
|
|
||||||
} else {
|
|
||||||
t29 = $[72];
|
|
||||||
}
|
|
||||||
t22 = t29;
|
|
||||||
break bb1;
|
|
||||||
}
|
|
||||||
T1 = Dialog;
|
|
||||||
t17 = sourceTitle;
|
|
||||||
let t23;
|
|
||||||
if ($[73] !== sortedAgents) {
|
|
||||||
t23 = count(sortedAgents, _temp8);
|
|
||||||
$[73] = sortedAgents;
|
|
||||||
$[74] = t23;
|
|
||||||
} else {
|
|
||||||
t23 = $[74];
|
|
||||||
}
|
|
||||||
t18 = `${t23} agents`;
|
|
||||||
t19 = onBack;
|
|
||||||
t20 = true;
|
|
||||||
if ($[75] !== changes) {
|
|
||||||
t21 = changes && changes.length > 0 && <Box marginTop={1}><Text dimColor={true}>{changes[changes.length - 1]}</Text></Box>;
|
|
||||||
$[75] = changes;
|
|
||||||
$[76] = t21;
|
|
||||||
} else {
|
|
||||||
t21 = $[76];
|
|
||||||
}
|
|
||||||
T0 = Box;
|
|
||||||
t11 = "column";
|
|
||||||
t12 = 0;
|
|
||||||
t13 = true;
|
|
||||||
t14 = handleKeyDown;
|
|
||||||
if ($[77] !== onCreateNew || $[78] !== renderCreateNewOption) {
|
|
||||||
t15 = onCreateNew && <Box marginBottom={1}>{renderCreateNewOption()}</Box>;
|
|
||||||
$[77] = onCreateNew;
|
|
||||||
$[78] = renderCreateNewOption;
|
|
||||||
$[79] = t15;
|
|
||||||
} else {
|
|
||||||
t15 = $[79];
|
|
||||||
}
|
|
||||||
t16 = source === "all" ? <>{AGENT_SOURCE_GROUPS.filter(_temp9).map(t24 => {
|
|
||||||
const {
|
|
||||||
label,
|
|
||||||
source: groupSource_0
|
|
||||||
} = t24;
|
|
||||||
return <React.Fragment key={groupSource_0}>{renderAgentGroup(label, sortedAgents.filter(a_7 => a_7.source === groupSource_0))}</React.Fragment>;
|
|
||||||
})}{builtInAgents_0.length > 0 && <Box flexDirection="column" marginBottom={1} paddingLeft={2}><Text dimColor={true}><Text bold={true}>Built-in agents</Text> (always available)</Text>{builtInAgents_0.map(renderAgent)}</Box>}</> : source === "built-in" ? <><Text dimColor={true} italic={true}>Built-in agents are provided by default and cannot be modified.</Text><Box marginTop={1} flexDirection="column">{sortedAgents.map(agent_2 => renderAgent(agent_2))}</Box></> : <>{sortedAgents.filter(_temp0).map(agent_3 => renderAgent(agent_3))}{sortedAgents.some(_temp1) && <><Divider />{renderBuiltInAgentsSection()}</>}</>;
|
|
||||||
}
|
}
|
||||||
$[30] = changes;
|
|
||||||
$[31] = handleKeyDown;
|
|
||||||
$[32] = onBack;
|
|
||||||
$[33] = onCreateNew;
|
|
||||||
$[34] = renderAgent;
|
|
||||||
$[35] = renderAgentGroup;
|
|
||||||
$[36] = renderBuiltInAgentsSection;
|
|
||||||
$[37] = renderCreateNewOption;
|
|
||||||
$[38] = sortedAgents;
|
|
||||||
$[39] = source;
|
|
||||||
$[40] = sourceTitle;
|
|
||||||
$[41] = T0;
|
|
||||||
$[42] = T1;
|
|
||||||
$[43] = t11;
|
|
||||||
$[44] = t12;
|
|
||||||
$[45] = t13;
|
|
||||||
$[46] = t14;
|
|
||||||
$[47] = t15;
|
|
||||||
$[48] = t16;
|
|
||||||
$[49] = t17;
|
|
||||||
$[50] = t18;
|
|
||||||
$[51] = t19;
|
|
||||||
$[52] = t20;
|
|
||||||
$[53] = t21;
|
|
||||||
$[54] = t22;
|
|
||||||
} else {
|
|
||||||
T0 = $[41];
|
|
||||||
T1 = $[42];
|
|
||||||
t11 = $[43];
|
|
||||||
t12 = $[44];
|
|
||||||
t13 = $[45];
|
|
||||||
t14 = $[46];
|
|
||||||
t15 = $[47];
|
|
||||||
t16 = $[48];
|
|
||||||
t17 = $[49];
|
|
||||||
t18 = $[50];
|
|
||||||
t19 = $[51];
|
|
||||||
t20 = $[52];
|
|
||||||
t21 = $[53];
|
|
||||||
t22 = $[54];
|
|
||||||
}
|
}
|
||||||
if (t22 !== Symbol.for("react.early_return_sentinel")) {
|
|
||||||
return t22;
|
const renderBuiltInAgentsSection = (
|
||||||
|
title = 'Built-in (always available):',
|
||||||
|
) => {
|
||||||
|
const builtInAgents = sortedAgents.filter(a => a.source === 'built-in')
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" marginBottom={1} paddingLeft={2}>
|
||||||
|
<Text bold dimColor>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
{builtInAgents.map(renderAgent)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
let t23;
|
|
||||||
if ($[80] !== T0 || $[81] !== t11 || $[82] !== t12 || $[83] !== t13 || $[84] !== t14 || $[85] !== t15 || $[86] !== t16) {
|
const renderAgentGroup = (title: string, groupAgents: ResolvedAgent[]) => {
|
||||||
t23 = <T0 flexDirection={t11} tabIndex={t12} autoFocus={t13} onKeyDown={t14}>{t15}{t16}</T0>;
|
if (!groupAgents.length) return null
|
||||||
$[80] = T0;
|
|
||||||
$[81] = t11;
|
const folderPath = groupAgents[0]?.baseDir
|
||||||
$[82] = t12;
|
|
||||||
$[83] = t13;
|
return (
|
||||||
$[84] = t14;
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
$[85] = t15;
|
<Box paddingLeft={2}>
|
||||||
$[86] = t16;
|
<Text bold dimColor>
|
||||||
$[87] = t23;
|
{title}
|
||||||
} else {
|
</Text>
|
||||||
t23 = $[87];
|
{folderPath && <Text dimColor> ({folderPath})</Text>}
|
||||||
|
</Box>
|
||||||
|
{groupAgents.map(agent => renderAgent(agent))}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
let t24;
|
|
||||||
if ($[88] !== T1 || $[89] !== t17 || $[90] !== t18 || $[91] !== t19 || $[92] !== t20 || $[93] !== t21 || $[94] !== t23) {
|
const sourceTitle = getAgentSourceDisplayName(source)
|
||||||
t24 = <T1 title={t17} subtitle={t18} onCancel={t19} hideInputGuide={t20}>{t21}{t23}</T1>;
|
|
||||||
$[88] = T1;
|
const builtInAgents = sortedAgents.filter(a => a.source === 'built-in')
|
||||||
$[89] = t17;
|
|
||||||
$[90] = t18;
|
const hasNoAgents =
|
||||||
$[91] = t19;
|
!sortedAgents.length ||
|
||||||
$[92] = t20;
|
(source !== 'built-in' && !sortedAgents.some(a => a.source !== 'built-in'))
|
||||||
$[93] = t21;
|
|
||||||
$[94] = t23;
|
if (hasNoAgents) {
|
||||||
$[95] = t24;
|
return (
|
||||||
} else {
|
<Dialog
|
||||||
t24 = $[95];
|
title={sourceTitle}
|
||||||
|
subtitle="No agents found"
|
||||||
|
onCancel={onBack}
|
||||||
|
hideInputGuide
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
gap={1}
|
||||||
|
tabIndex={0}
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
{onCreateNew && <Box>{renderCreateNewOption()}</Box>}
|
||||||
|
<Text dimColor>
|
||||||
|
No agents found. Create specialized subagents that Claude can
|
||||||
|
delegate to.
|
||||||
|
</Text>
|
||||||
|
<Text dimColor>
|
||||||
|
Each subagent has its own context window, custom system prompt, and
|
||||||
|
specific tools.
|
||||||
|
</Text>
|
||||||
|
<Text dimColor>
|
||||||
|
Try creating: Code Reviewer, Code Simplifier, Security Reviewer,
|
||||||
|
Tech Lead, or UX Reviewer.
|
||||||
|
</Text>
|
||||||
|
{source !== 'built-in' &&
|
||||||
|
sortedAgents.some(a => a.source === 'built-in') && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
{renderBuiltInAgentsSection()}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return t24;
|
|
||||||
}
|
return (
|
||||||
function _temp1(a_9) {
|
<Dialog
|
||||||
return a_9.source === "built-in";
|
title={sourceTitle}
|
||||||
}
|
subtitle={`${count(sortedAgents, a => !a.overriddenBy)} agents`}
|
||||||
function _temp0(a_8) {
|
onCancel={onBack}
|
||||||
return a_8.source !== "built-in";
|
hideInputGuide
|
||||||
}
|
>
|
||||||
function _temp9(g_0) {
|
{changes && changes.length > 0 && (
|
||||||
return g_0.source !== "built-in";
|
<Box marginTop={1}>
|
||||||
}
|
<Text dimColor>{changes[changes.length - 1]}</Text>
|
||||||
function _temp8(a_6) {
|
</Box>
|
||||||
return !a_6.overriddenBy;
|
)}
|
||||||
}
|
<Box
|
||||||
function _temp7(a_5) {
|
flexDirection="column"
|
||||||
return a_5.source === "built-in";
|
tabIndex={0}
|
||||||
}
|
autoFocus
|
||||||
function _temp6(a_4) {
|
onKeyDown={handleKeyDown}
|
||||||
return a_4.source !== "built-in";
|
>
|
||||||
}
|
{onCreateNew && <Box marginBottom={1}>{renderCreateNewOption()}</Box>}
|
||||||
function _temp5(a_3) {
|
{source === 'all' ? (
|
||||||
return a_3.source === "built-in";
|
<>
|
||||||
}
|
{AGENT_SOURCE_GROUPS.filter(g => g.source !== 'built-in').map(
|
||||||
function _temp4(a_2) {
|
({ label, source: groupSource }) => (
|
||||||
return a_2.source === "built-in";
|
<React.Fragment key={groupSource}>
|
||||||
}
|
{renderAgentGroup(
|
||||||
function _temp3(g) {
|
label,
|
||||||
return g.source !== "built-in";
|
sortedAgents.filter(a => a.source === groupSource),
|
||||||
}
|
)}
|
||||||
function _temp2(a) {
|
</React.Fragment>
|
||||||
return a.source !== "built-in";
|
),
|
||||||
}
|
)}
|
||||||
function _temp(agent) {
|
{builtInAgents.length > 0 && (
|
||||||
return {
|
<Box flexDirection="column" marginBottom={1} paddingLeft={2}>
|
||||||
isOverridden: !!agent.overriddenBy,
|
<Text dimColor>
|
||||||
overriddenBy: agent.overriddenBy || null
|
<Text bold>Built-in agents</Text> (always available)
|
||||||
};
|
</Text>
|
||||||
|
{builtInAgents.map(renderAgent)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : source === 'built-in' ? (
|
||||||
|
<>
|
||||||
|
<Text dimColor italic>
|
||||||
|
Built-in agents are provided by default and cannot be modified.
|
||||||
|
</Text>
|
||||||
|
<Box marginTop={1} flexDirection="column">
|
||||||
|
{sortedAgents.map(agent => renderAgent(agent))}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{sortedAgents
|
||||||
|
.filter(a => a.source !== 'built-in')
|
||||||
|
.map(agent => renderAgent(agent))}
|
||||||
|
{sortedAgents.some(a => a.source === 'built-in') && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
{renderBuiltInAgentsSection()}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,111 +1,106 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import figures from 'figures'
|
||||||
import figures from 'figures';
|
import React, { useState } from 'react'
|
||||||
import React, { useState } from 'react';
|
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import {
|
||||||
import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from '../../tools/AgentTool/agentColorManager.js';
|
AGENT_COLOR_TO_THEME_COLOR,
|
||||||
import { capitalize } from '../../utils/stringUtils.js';
|
AGENT_COLORS,
|
||||||
type ColorOption = AgentColorName | 'automatic';
|
type AgentColorName,
|
||||||
const COLOR_OPTIONS: ColorOption[] = ['automatic', ...AGENT_COLORS];
|
} from '../../tools/AgentTool/agentColorManager.js'
|
||||||
|
import { capitalize } from '../../utils/stringUtils.js'
|
||||||
|
|
||||||
|
type ColorOption = AgentColorName | 'automatic'
|
||||||
|
|
||||||
|
const COLOR_OPTIONS: ColorOption[] = ['automatic', ...AGENT_COLORS]
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
agentName: string;
|
agentName: string
|
||||||
currentColor?: AgentColorName | 'automatic';
|
currentColor?: AgentColorName | 'automatic'
|
||||||
onConfirm: (color: AgentColorName | undefined) => void;
|
onConfirm: (color: AgentColorName | undefined) => void
|
||||||
};
|
|
||||||
export function ColorPicker(t0) {
|
|
||||||
const $ = _c(17);
|
|
||||||
const {
|
|
||||||
agentName,
|
|
||||||
currentColor: t1,
|
|
||||||
onConfirm
|
|
||||||
} = t0;
|
|
||||||
const currentColor = t1 === undefined ? "automatic" : t1;
|
|
||||||
let t2;
|
|
||||||
if ($[0] !== currentColor) {
|
|
||||||
t2 = COLOR_OPTIONS.findIndex(opt => opt === currentColor);
|
|
||||||
$[0] = currentColor;
|
|
||||||
$[1] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[1];
|
|
||||||
}
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(Math.max(0, t2));
|
|
||||||
let t3;
|
|
||||||
if ($[2] !== onConfirm || $[3] !== selectedIndex) {
|
|
||||||
t3 = e => {
|
|
||||||
if (e.key === "up") {
|
|
||||||
e.preventDefault();
|
|
||||||
setSelectedIndex(_temp);
|
|
||||||
} else {
|
|
||||||
if (e.key === "down") {
|
|
||||||
e.preventDefault();
|
|
||||||
setSelectedIndex(_temp2);
|
|
||||||
} else {
|
|
||||||
if (e.key === "return") {
|
|
||||||
e.preventDefault();
|
|
||||||
const selected = COLOR_OPTIONS[selectedIndex];
|
|
||||||
onConfirm(selected === "automatic" ? undefined : selected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$[2] = onConfirm;
|
|
||||||
$[3] = selectedIndex;
|
|
||||||
$[4] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[4];
|
|
||||||
}
|
|
||||||
const handleKeyDown = t3;
|
|
||||||
const selectedValue = COLOR_OPTIONS[selectedIndex];
|
|
||||||
let t4;
|
|
||||||
if ($[5] !== selectedIndex) {
|
|
||||||
t4 = COLOR_OPTIONS.map((option, index) => {
|
|
||||||
const isSelected = index === selectedIndex;
|
|
||||||
return <Box key={option} flexDirection="row" gap={1}><Text color={isSelected ? "suggestion" : undefined}>{isSelected ? figures.pointer : " "}</Text>{option === "automatic" ? <Text bold={isSelected}>Automatic color</Text> : <Box gap={1}><Text backgroundColor={AGENT_COLOR_TO_THEME_COLOR[option]} color="inverseText">{" "}</Text><Text bold={isSelected}>{capitalize(option)}</Text></Box>}</Box>;
|
|
||||||
});
|
|
||||||
$[5] = selectedIndex;
|
|
||||||
$[6] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[6];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[7] !== t4) {
|
|
||||||
t5 = <Box flexDirection="column">{t4}</Box>;
|
|
||||||
$[7] = t4;
|
|
||||||
$[8] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[8];
|
|
||||||
}
|
|
||||||
let t6;
|
|
||||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t6 = <Text>Preview: </Text>;
|
|
||||||
$[9] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[9];
|
|
||||||
}
|
|
||||||
let t7;
|
|
||||||
if ($[10] !== agentName || $[11] !== selectedValue) {
|
|
||||||
t7 = <Box marginTop={1}>{t6}{selectedValue === undefined || selectedValue === "automatic" ? <Text inverse={true} bold={true}>{" "}@{agentName}{" "}</Text> : <Text backgroundColor={AGENT_COLOR_TO_THEME_COLOR[selectedValue]} color="inverseText" bold={true}>{" "}@{agentName}{" "}</Text>}</Box>;
|
|
||||||
$[10] = agentName;
|
|
||||||
$[11] = selectedValue;
|
|
||||||
$[12] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[12];
|
|
||||||
}
|
|
||||||
let t8;
|
|
||||||
if ($[13] !== handleKeyDown || $[14] !== t5 || $[15] !== t7) {
|
|
||||||
t8 = <Box flexDirection="column" gap={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t5}{t7}</Box>;
|
|
||||||
$[13] = handleKeyDown;
|
|
||||||
$[14] = t5;
|
|
||||||
$[15] = t7;
|
|
||||||
$[16] = t8;
|
|
||||||
} else {
|
|
||||||
t8 = $[16];
|
|
||||||
}
|
|
||||||
return t8;
|
|
||||||
}
|
}
|
||||||
function _temp2(prev_0) {
|
|
||||||
return prev_0 < COLOR_OPTIONS.length - 1 ? prev_0 + 1 : 0;
|
export function ColorPicker({
|
||||||
}
|
agentName,
|
||||||
function _temp(prev) {
|
currentColor = 'automatic',
|
||||||
return prev > 0 ? prev - 1 : COLOR_OPTIONS.length - 1;
|
onConfirm,
|
||||||
|
}: Props): React.ReactNode {
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
COLOR_OPTIONS.findIndex(opt => opt === currentColor),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'up') {
|
||||||
|
e.preventDefault()
|
||||||
|
setSelectedIndex(prev => (prev > 0 ? prev - 1 : COLOR_OPTIONS.length - 1))
|
||||||
|
} else if (e.key === 'down') {
|
||||||
|
e.preventDefault()
|
||||||
|
setSelectedIndex(prev => (prev < COLOR_OPTIONS.length - 1 ? prev + 1 : 0))
|
||||||
|
} else if (e.key === 'return') {
|
||||||
|
e.preventDefault()
|
||||||
|
const selected = COLOR_OPTIONS[selectedIndex]
|
||||||
|
onConfirm(selected === 'automatic' ? undefined : selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedValue = COLOR_OPTIONS[selectedIndex]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
gap={1}
|
||||||
|
tabIndex={0}
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
<Box flexDirection="column">
|
||||||
|
{COLOR_OPTIONS.map((option, index) => {
|
||||||
|
const isSelected = index === selectedIndex
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={option} flexDirection="row" gap={1}>
|
||||||
|
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||||
|
{isSelected ? figures.pointer : ' '}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{option === 'automatic' ? (
|
||||||
|
<Text bold={isSelected}>Automatic color</Text>
|
||||||
|
) : (
|
||||||
|
<Box gap={1}>
|
||||||
|
<Text
|
||||||
|
backgroundColor={AGENT_COLOR_TO_THEME_COLOR[option]}
|
||||||
|
color="inverseText"
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
</Text>
|
||||||
|
<Text bold={isSelected}>{capitalize(option)}</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text>Preview: </Text>
|
||||||
|
{selectedValue === undefined || selectedValue === 'automatic' ? (
|
||||||
|
<Text inverse bold>
|
||||||
|
{' '}
|
||||||
|
@{agentName}{' '}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
backgroundColor={AGENT_COLOR_TO_THEME_COLOR[selectedValue]}
|
||||||
|
color="inverseText"
|
||||||
|
bold
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
@{agentName}{' '}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,52 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import { getAgentModelOptions } from '../../utils/model/agent.js'
|
||||||
import { getAgentModelOptions } from '../../utils/model/agent.js';
|
import { Select } from '../CustomSelect/select.js'
|
||||||
import { Select } from '../CustomSelect/select.js';
|
|
||||||
interface ModelSelectorProps {
|
interface ModelSelectorProps {
|
||||||
initialModel?: string;
|
initialModel?: string
|
||||||
onComplete: (model?: string) => void;
|
onComplete: (model?: string) => void
|
||||||
onCancel?: () => void;
|
onCancel?: () => void
|
||||||
}
|
}
|
||||||
export function ModelSelector(t0) {
|
|
||||||
const $ = _c(11);
|
export function ModelSelector({
|
||||||
const {
|
initialModel,
|
||||||
initialModel,
|
onComplete,
|
||||||
onComplete,
|
onCancel,
|
||||||
onCancel
|
}: ModelSelectorProps): React.ReactNode {
|
||||||
} = t0;
|
const modelOptions = React.useMemo(() => {
|
||||||
let t1;
|
const base = getAgentModelOptions()
|
||||||
if ($[0] !== initialModel) {
|
// If the agent's current model is a full ID (e.g. 'claude-opus-4-5') not
|
||||||
bb0: {
|
// in the alias list, inject it as an option so it can round-trip through
|
||||||
const base = getAgentModelOptions();
|
// confirm without being overwritten.
|
||||||
if (initialModel && !base.some(o => o.value === initialModel)) {
|
if (initialModel && !base.some(o => o.value === initialModel)) {
|
||||||
t1 = [{
|
return [
|
||||||
|
{
|
||||||
value: initialModel,
|
value: initialModel,
|
||||||
label: initialModel,
|
label: initialModel,
|
||||||
description: "Current model (custom ID)"
|
description: 'Current model (custom ID)',
|
||||||
}, ...base];
|
},
|
||||||
break bb0;
|
...base,
|
||||||
}
|
]
|
||||||
t1 = base;
|
|
||||||
}
|
}
|
||||||
$[0] = initialModel;
|
return base
|
||||||
$[1] = t1;
|
}, [initialModel])
|
||||||
} else {
|
|
||||||
t1 = $[1];
|
const defaultModel = initialModel ?? 'sonnet'
|
||||||
}
|
|
||||||
const modelOptions = t1;
|
return (
|
||||||
const defaultModel = initialModel ?? "sonnet";
|
<Box flexDirection="column">
|
||||||
let t2;
|
<Box marginBottom={1}>
|
||||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
<Text dimColor>
|
||||||
t2 = <Box marginBottom={1}><Text dimColor={true}>Model determines the agent's reasoning capabilities and speed.</Text></Box>;
|
Model determines the agent's reasoning capabilities and speed.
|
||||||
$[2] = t2;
|
</Text>
|
||||||
} else {
|
</Box>
|
||||||
t2 = $[2];
|
<Select
|
||||||
}
|
options={modelOptions}
|
||||||
let t3;
|
defaultValue={defaultModel}
|
||||||
if ($[3] !== onCancel || $[4] !== onComplete) {
|
onChange={onComplete}
|
||||||
t3 = () => onCancel ? onCancel() : onComplete(undefined);
|
onCancel={() => (onCancel ? onCancel() : onComplete(undefined))}
|
||||||
$[3] = onCancel;
|
/>
|
||||||
$[4] = onComplete;
|
</Box>
|
||||||
$[5] = t3;
|
)
|
||||||
} else {
|
|
||||||
t3 = $[5];
|
|
||||||
}
|
|
||||||
let t4;
|
|
||||||
if ($[6] !== defaultModel || $[7] !== modelOptions || $[8] !== onComplete || $[9] !== t3) {
|
|
||||||
t4 = <Box flexDirection="column">{t2}<Select options={modelOptions} defaultValue={defaultModel} onChange={onComplete} onCancel={t3} /></Box>;
|
|
||||||
$[6] = defaultModel;
|
|
||||||
$[7] = modelOptions;
|
|
||||||
$[8] = onComplete;
|
|
||||||
$[9] = t3;
|
|
||||||
$[10] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[10];
|
|
||||||
}
|
|
||||||
return t4;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,561 +1,478 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import figures from 'figures'
|
||||||
import figures from 'figures';
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import { mcpInfoFromString } from 'src/services/mcp/mcpStringUtils.js'
|
||||||
import { mcpInfoFromString } from 'src/services/mcp/mcpStringUtils.js';
|
import { isMcpTool } from 'src/services/mcp/utils.js'
|
||||||
import { isMcpTool } from 'src/services/mcp/utils.js';
|
import type { Tool, Tools } from 'src/Tool.js'
|
||||||
import type { Tool, Tools } from 'src/Tool.js';
|
import { filterToolsForAgent } from 'src/tools/AgentTool/agentToolUtils.js'
|
||||||
import { filterToolsForAgent } from 'src/tools/AgentTool/agentToolUtils.js';
|
import { AGENT_TOOL_NAME } from 'src/tools/AgentTool/constants.js'
|
||||||
import { AGENT_TOOL_NAME } from 'src/tools/AgentTool/constants.js';
|
import { BashTool } from 'src/tools/BashTool/BashTool.js'
|
||||||
import { BashTool } from 'src/tools/BashTool/BashTool.js';
|
import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
|
||||||
import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js';
|
import { FileEditTool } from 'src/tools/FileEditTool/FileEditTool.js'
|
||||||
import { FileEditTool } from 'src/tools/FileEditTool/FileEditTool.js';
|
import { FileReadTool } from 'src/tools/FileReadTool/FileReadTool.js'
|
||||||
import { FileReadTool } from 'src/tools/FileReadTool/FileReadTool.js';
|
import { FileWriteTool } from 'src/tools/FileWriteTool/FileWriteTool.js'
|
||||||
import { FileWriteTool } from 'src/tools/FileWriteTool/FileWriteTool.js';
|
import { GlobTool } from 'src/tools/GlobTool/GlobTool.js'
|
||||||
import { GlobTool } from 'src/tools/GlobTool/GlobTool.js';
|
import { GrepTool } from 'src/tools/GrepTool/GrepTool.js'
|
||||||
import { GrepTool } from 'src/tools/GrepTool/GrepTool.js';
|
import { ListMcpResourcesTool } from 'src/tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
|
||||||
import { ListMcpResourcesTool } from 'src/tools/ListMcpResourcesTool/ListMcpResourcesTool.js';
|
import { NotebookEditTool } from 'src/tools/NotebookEditTool/NotebookEditTool.js'
|
||||||
import { NotebookEditTool } from 'src/tools/NotebookEditTool/NotebookEditTool.js';
|
import { ReadMcpResourceTool } from 'src/tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
|
||||||
import { ReadMcpResourceTool } from 'src/tools/ReadMcpResourceTool/ReadMcpResourceTool.js';
|
import { TaskOutputTool } from 'src/tools/TaskOutputTool/TaskOutputTool.js'
|
||||||
import { TaskOutputTool } from 'src/tools/TaskOutputTool/TaskOutputTool.js';
|
import { TaskStopTool } from 'src/tools/TaskStopTool/TaskStopTool.js'
|
||||||
import { TaskStopTool } from 'src/tools/TaskStopTool/TaskStopTool.js';
|
import { TodoWriteTool } from 'src/tools/TodoWriteTool/TodoWriteTool.js'
|
||||||
import { TodoWriteTool } from 'src/tools/TodoWriteTool/TodoWriteTool.js';
|
import { TungstenTool } from 'src/tools/TungstenTool/TungstenTool.js'
|
||||||
import { TungstenTool } from 'src/tools/TungstenTool/TungstenTool.js';
|
import { WebFetchTool } from 'src/tools/WebFetchTool/WebFetchTool.js'
|
||||||
import { WebFetchTool } from 'src/tools/WebFetchTool/WebFetchTool.js';
|
import { WebSearchTool } from 'src/tools/WebSearchTool/WebSearchTool.js'
|
||||||
import { WebSearchTool } from 'src/tools/WebSearchTool/WebSearchTool.js';
|
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
import { count } from '../../utils/array.js'
|
||||||
import { count } from '../../utils/array.js';
|
import { plural } from '../../utils/stringUtils.js'
|
||||||
import { plural } from '../../utils/stringUtils.js';
|
import { Divider } from '../design-system/Divider.js'
|
||||||
import { Divider } from '../design-system/Divider.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tools: Tools;
|
tools: Tools
|
||||||
initialTools: string[] | undefined;
|
initialTools: string[] | undefined
|
||||||
onComplete: (selectedTools: string[] | undefined) => void;
|
onComplete: (selectedTools: string[] | undefined) => void
|
||||||
onCancel?: () => void;
|
onCancel?: () => void
|
||||||
};
|
}
|
||||||
|
|
||||||
type ToolBucket = {
|
type ToolBucket = {
|
||||||
name: string;
|
name: string
|
||||||
toolNames: Set<string>;
|
toolNames: Set<string>
|
||||||
isMcp?: boolean;
|
isMcp?: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
type ToolBuckets = {
|
type ToolBuckets = {
|
||||||
READ_ONLY: ToolBucket;
|
READ_ONLY: ToolBucket
|
||||||
EDIT: ToolBucket;
|
EDIT: ToolBucket
|
||||||
EXECUTION: ToolBucket;
|
EXECUTION: ToolBucket
|
||||||
MCP: ToolBucket;
|
MCP: ToolBucket
|
||||||
OTHER: ToolBucket;
|
OTHER: ToolBucket
|
||||||
};
|
}
|
||||||
|
|
||||||
function getToolBuckets(): ToolBuckets {
|
function getToolBuckets(): ToolBuckets {
|
||||||
return {
|
return {
|
||||||
READ_ONLY: {
|
READ_ONLY: {
|
||||||
name: 'Read-only tools',
|
name: 'Read-only tools',
|
||||||
toolNames: new Set([GlobTool.name, GrepTool.name, ExitPlanModeV2Tool.name, FileReadTool.name, WebFetchTool.name, TodoWriteTool.name, WebSearchTool.name, TaskStopTool.name, TaskOutputTool.name, ListMcpResourcesTool.name, ReadMcpResourceTool.name])
|
toolNames: new Set([
|
||||||
|
GlobTool.name,
|
||||||
|
GrepTool.name,
|
||||||
|
ExitPlanModeV2Tool.name,
|
||||||
|
FileReadTool.name,
|
||||||
|
WebFetchTool.name,
|
||||||
|
TodoWriteTool.name,
|
||||||
|
WebSearchTool.name,
|
||||||
|
TaskStopTool.name,
|
||||||
|
TaskOutputTool.name,
|
||||||
|
ListMcpResourcesTool.name,
|
||||||
|
ReadMcpResourceTool.name,
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
EDIT: {
|
EDIT: {
|
||||||
name: 'Edit tools',
|
name: 'Edit tools',
|
||||||
toolNames: new Set([FileEditTool.name, FileWriteTool.name, NotebookEditTool.name])
|
toolNames: new Set([
|
||||||
|
FileEditTool.name,
|
||||||
|
FileWriteTool.name,
|
||||||
|
NotebookEditTool.name,
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
EXECUTION: {
|
EXECUTION: {
|
||||||
name: 'Execution tools',
|
name: 'Execution tools',
|
||||||
toolNames: new Set([BashTool.name, (process.env.USER_TYPE) === 'ant' ? TungstenTool.name : undefined].filter(n => n !== undefined))
|
toolNames: new Set(
|
||||||
|
[
|
||||||
|
BashTool.name,
|
||||||
|
process.env.USER_TYPE === 'ant' ? TungstenTool.name : undefined,
|
||||||
|
].filter(n => n !== undefined),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
MCP: {
|
MCP: {
|
||||||
name: 'MCP tools',
|
name: 'MCP tools',
|
||||||
toolNames: new Set(),
|
toolNames: new Set(), // Dynamic - no static list
|
||||||
// Dynamic - no static list
|
isMcp: true,
|
||||||
isMcp: true
|
|
||||||
},
|
},
|
||||||
OTHER: {
|
OTHER: {
|
||||||
name: 'Other tools',
|
name: 'Other tools',
|
||||||
toolNames: new Set() // Dynamic - catch-all for uncategorized tools
|
toolNames: new Set(), // Dynamic - catch-all for uncategorized tools
|
||||||
}
|
},
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to get MCP server buckets dynamically
|
// Helper to get MCP server buckets dynamically
|
||||||
function getMcpServerBuckets(tools: Tools): Array<{
|
function getMcpServerBuckets(tools: Tools): Array<{
|
||||||
serverName: string;
|
serverName: string
|
||||||
tools: Tools;
|
tools: Tools
|
||||||
}> {
|
}> {
|
||||||
const serverMap = new Map<string, Tool[]>();
|
const serverMap = new Map<string, Tool[]>()
|
||||||
|
|
||||||
tools.forEach(tool => {
|
tools.forEach(tool => {
|
||||||
if (isMcpTool(tool)) {
|
if (isMcpTool(tool)) {
|
||||||
const mcpInfo = mcpInfoFromString(tool.name);
|
const mcpInfo = mcpInfoFromString(tool.name)
|
||||||
if (mcpInfo?.serverName) {
|
if (mcpInfo?.serverName) {
|
||||||
const existing = serverMap.get(mcpInfo.serverName) || [];
|
const existing = serverMap.get(mcpInfo.serverName) || []
|
||||||
existing.push(tool);
|
existing.push(tool)
|
||||||
serverMap.set(mcpInfo.serverName, existing);
|
serverMap.set(mcpInfo.serverName, existing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
return Array.from(serverMap.entries()).map(([serverName, tools]) => ({
|
|
||||||
serverName,
|
return Array.from(serverMap.entries())
|
||||||
tools
|
.map(([serverName, tools]) => ({ serverName, tools }))
|
||||||
})).sort((a, b) => a.serverName.localeCompare(b.serverName));
|
.sort((a, b) => a.serverName.localeCompare(b.serverName))
|
||||||
}
|
}
|
||||||
export function ToolSelector(t0) {
|
|
||||||
const $ = _c(69);
|
export function ToolSelector({
|
||||||
const {
|
tools,
|
||||||
tools,
|
initialTools,
|
||||||
initialTools,
|
onComplete,
|
||||||
onComplete,
|
onCancel,
|
||||||
onCancel
|
}: Props): React.ReactNode {
|
||||||
} = t0;
|
// Filter tools for custom agents
|
||||||
let t1;
|
const customAgentTools = useMemo(
|
||||||
if ($[0] !== tools) {
|
() => filterToolsForAgent({ tools, isBuiltIn: false, isAsync: false }),
|
||||||
t1 = filterToolsForAgent({
|
[tools],
|
||||||
tools,
|
)
|
||||||
isBuiltIn: false,
|
|
||||||
isAsync: false
|
// Expand wildcard or undefined to explicit tool list for internal state
|
||||||
});
|
const expandedInitialTools =
|
||||||
$[0] = tools;
|
!initialTools || initialTools.includes('*')
|
||||||
$[1] = t1;
|
? customAgentTools.map(t => t.name)
|
||||||
} else {
|
: initialTools
|
||||||
t1 = $[1];
|
|
||||||
|
const [selectedTools, setSelectedTools] =
|
||||||
|
useState<string[]>(expandedInitialTools)
|
||||||
|
const [focusIndex, setFocusIndex] = useState(0)
|
||||||
|
const [showIndividualTools, setShowIndividualTools] = useState(false)
|
||||||
|
|
||||||
|
// Filter selectedTools to only include tools that currently exist
|
||||||
|
// This handles MCP tools that disconnect while selected
|
||||||
|
const validSelectedTools = useMemo(() => {
|
||||||
|
const toolNames = new Set(customAgentTools.map(t => t.name))
|
||||||
|
return selectedTools.filter(name => toolNames.has(name))
|
||||||
|
}, [selectedTools, customAgentTools])
|
||||||
|
|
||||||
|
const selectedSet = new Set(validSelectedTools)
|
||||||
|
const isAllSelected =
|
||||||
|
validSelectedTools.length === customAgentTools.length &&
|
||||||
|
customAgentTools.length > 0
|
||||||
|
|
||||||
|
const handleToggleTool = (toolName: string) => {
|
||||||
|
if (!toolName) return
|
||||||
|
|
||||||
|
setSelectedTools(current =>
|
||||||
|
current.includes(toolName)
|
||||||
|
? current.filter(t => t !== toolName)
|
||||||
|
: [...current, toolName],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const customAgentTools = t1;
|
|
||||||
let t2;
|
const handleToggleTools = (toolNames: string[], select: boolean) => {
|
||||||
if ($[2] !== customAgentTools || $[3] !== initialTools) {
|
setSelectedTools(current => {
|
||||||
t2 = !initialTools || initialTools.includes("*") ? customAgentTools.map(_temp) : initialTools;
|
if (select) {
|
||||||
$[2] = customAgentTools;
|
const toolsToAdd = toolNames.filter(t => !current.includes(t))
|
||||||
$[3] = initialTools;
|
return [...current, ...toolsToAdd]
|
||||||
$[4] = t2;
|
} else {
|
||||||
} else {
|
return current.filter(t => !toolNames.includes(t))
|
||||||
t2 = $[4];
|
|
||||||
}
|
|
||||||
const expandedInitialTools = t2;
|
|
||||||
const [selectedTools, setSelectedTools] = useState(expandedInitialTools);
|
|
||||||
const [focusIndex, setFocusIndex] = useState(0);
|
|
||||||
const [showIndividualTools, setShowIndividualTools] = useState(false);
|
|
||||||
let t3;
|
|
||||||
if ($[5] !== customAgentTools) {
|
|
||||||
t3 = new Set(customAgentTools.map(_temp2));
|
|
||||||
$[5] = customAgentTools;
|
|
||||||
$[6] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[6];
|
|
||||||
}
|
|
||||||
const toolNames = t3;
|
|
||||||
let t4;
|
|
||||||
if ($[7] !== selectedTools || $[8] !== toolNames) {
|
|
||||||
let t5;
|
|
||||||
if ($[10] !== toolNames) {
|
|
||||||
t5 = name => toolNames.has(name);
|
|
||||||
$[10] = toolNames;
|
|
||||||
$[11] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[11];
|
|
||||||
}
|
|
||||||
t4 = selectedTools.filter(t5);
|
|
||||||
$[7] = selectedTools;
|
|
||||||
$[8] = toolNames;
|
|
||||||
$[9] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[9];
|
|
||||||
}
|
|
||||||
const validSelectedTools = t4;
|
|
||||||
let t5;
|
|
||||||
if ($[12] !== validSelectedTools) {
|
|
||||||
t5 = new Set(validSelectedTools);
|
|
||||||
$[12] = validSelectedTools;
|
|
||||||
$[13] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[13];
|
|
||||||
}
|
|
||||||
const selectedSet = t5;
|
|
||||||
const isAllSelected = validSelectedTools.length === customAgentTools.length && customAgentTools.length > 0;
|
|
||||||
let t6;
|
|
||||||
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t6 = toolName => {
|
|
||||||
if (!toolName) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
setSelectedTools(current => current.includes(toolName) ? current.filter(t_1 => t_1 !== toolName) : [...current, toolName]);
|
})
|
||||||
};
|
|
||||||
$[14] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[14];
|
|
||||||
}
|
}
|
||||||
const handleToggleTool = t6;
|
|
||||||
let t7;
|
const handleConfirm = () => {
|
||||||
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
|
// Convert to undefined if all tools are selected (for cleaner file format)
|
||||||
t7 = (toolNames_0, select) => {
|
const allToolNames = customAgentTools.map(t => t.name)
|
||||||
setSelectedTools(current_0 => {
|
const areAllToolsSelected =
|
||||||
if (select) {
|
validSelectedTools.length === allToolNames.length &&
|
||||||
const toolsToAdd = toolNames_0.filter(t_2 => !current_0.includes(t_2));
|
allToolNames.every(name => validSelectedTools.includes(name))
|
||||||
return [...current_0, ...toolsToAdd];
|
const finalTools = areAllToolsSelected ? undefined : validSelectedTools
|
||||||
} else {
|
|
||||||
return current_0.filter(t_3 => !toolNames_0.includes(t_3));
|
onComplete(finalTools)
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
$[15] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[15];
|
|
||||||
}
|
}
|
||||||
const handleToggleTools = t7;
|
|
||||||
let t8;
|
// Group tools by bucket
|
||||||
if ($[16] !== customAgentTools || $[17] !== onComplete || $[18] !== validSelectedTools) {
|
const toolsByBucket = useMemo(() => {
|
||||||
t8 = () => {
|
const toolBuckets = getToolBuckets()
|
||||||
const allToolNames = customAgentTools.map(_temp3);
|
const buckets = {
|
||||||
const areAllToolsSelected = validSelectedTools.length === allToolNames.length && allToolNames.every(name_0 => validSelectedTools.includes(name_0));
|
|
||||||
const finalTools = areAllToolsSelected ? undefined : validSelectedTools;
|
|
||||||
onComplete(finalTools);
|
|
||||||
};
|
|
||||||
$[16] = customAgentTools;
|
|
||||||
$[17] = onComplete;
|
|
||||||
$[18] = validSelectedTools;
|
|
||||||
$[19] = t8;
|
|
||||||
} else {
|
|
||||||
t8 = $[19];
|
|
||||||
}
|
|
||||||
const handleConfirm = t8;
|
|
||||||
let buckets;
|
|
||||||
if ($[20] !== customAgentTools) {
|
|
||||||
const toolBuckets = getToolBuckets();
|
|
||||||
buckets = {
|
|
||||||
readOnly: [] as Tool[],
|
readOnly: [] as Tool[],
|
||||||
edit: [] as Tool[],
|
edit: [] as Tool[],
|
||||||
execution: [] as Tool[],
|
execution: [] as Tool[],
|
||||||
mcp: [] as Tool[],
|
mcp: [] as Tool[],
|
||||||
other: [] as Tool[]
|
other: [] as Tool[],
|
||||||
};
|
}
|
||||||
|
|
||||||
customAgentTools.forEach(tool => {
|
customAgentTools.forEach(tool => {
|
||||||
|
// Check if it's an MCP tool first
|
||||||
if (isMcpTool(tool)) {
|
if (isMcpTool(tool)) {
|
||||||
buckets.mcp.push(tool);
|
buckets.mcp.push(tool)
|
||||||
} else {
|
} else if (toolBuckets.READ_ONLY.toolNames.has(tool.name)) {
|
||||||
if (toolBuckets.READ_ONLY.toolNames.has(tool.name)) {
|
buckets.readOnly.push(tool)
|
||||||
buckets.readOnly.push(tool);
|
} else if (toolBuckets.EDIT.toolNames.has(tool.name)) {
|
||||||
} else {
|
buckets.edit.push(tool)
|
||||||
if (toolBuckets.EDIT.toolNames.has(tool.name)) {
|
} else if (toolBuckets.EXECUTION.toolNames.has(tool.name)) {
|
||||||
buckets.edit.push(tool);
|
buckets.execution.push(tool)
|
||||||
} else {
|
} else if (tool.name !== AGENT_TOOL_NAME) {
|
||||||
if (toolBuckets.EXECUTION.toolNames.has(tool.name)) {
|
// Catch-all for uncategorized tools (except Task)
|
||||||
buckets.execution.push(tool);
|
buckets.other.push(tool)
|
||||||
} else {
|
|
||||||
if (tool.name !== AGENT_TOOL_NAME) {
|
|
||||||
buckets.other.push(tool);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
$[20] = customAgentTools;
|
|
||||||
$[21] = buckets;
|
return buckets
|
||||||
} else {
|
}, [customAgentTools])
|
||||||
buckets = $[21];
|
|
||||||
}
|
const createBucketToggleAction = (bucketTools: Tool[]) => {
|
||||||
const toolsByBucket = buckets;
|
const selected = count(bucketTools, t => selectedSet.has(t.name))
|
||||||
let t9;
|
const needsSelection = selected < bucketTools.length
|
||||||
if ($[22] !== selectedSet) {
|
|
||||||
t9 = bucketTools => {
|
return () => {
|
||||||
const selected = count(bucketTools, (t_5: Tool) => selectedSet.has(t_5.name));
|
const toolNames = bucketTools.map(t => t.name)
|
||||||
const needsSelection = selected < bucketTools.length;
|
handleToggleTools(toolNames, needsSelection)
|
||||||
return () => {
|
|
||||||
const toolNames_1 = bucketTools.map(_temp4);
|
|
||||||
handleToggleTools(toolNames_1, needsSelection);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
$[22] = selectedSet;
|
|
||||||
$[23] = t9;
|
|
||||||
} else {
|
|
||||||
t9 = $[23];
|
|
||||||
}
|
|
||||||
const createBucketToggleAction = t9;
|
|
||||||
let navigableItems;
|
|
||||||
if ($[24] !== createBucketToggleAction || $[25] !== customAgentTools || $[26] !== focusIndex || $[27] !== handleConfirm || $[28] !== isAllSelected || $[29] !== selectedSet || $[30] !== showIndividualTools || $[31] !== toolsByBucket.edit || $[32] !== toolsByBucket.execution || $[33] !== toolsByBucket.mcp || $[34] !== toolsByBucket.other || $[35] !== toolsByBucket.readOnly) {
|
|
||||||
navigableItems = [];
|
|
||||||
navigableItems.push({
|
|
||||||
id: "continue",
|
|
||||||
label: "Continue",
|
|
||||||
action: handleConfirm,
|
|
||||||
isContinue: true
|
|
||||||
});
|
|
||||||
let t10;
|
|
||||||
if ($[37] !== customAgentTools || $[38] !== isAllSelected) {
|
|
||||||
t10 = () => {
|
|
||||||
const allToolNames_0 = customAgentTools.map(_temp5);
|
|
||||||
handleToggleTools(allToolNames_0, !isAllSelected);
|
|
||||||
};
|
|
||||||
$[37] = customAgentTools;
|
|
||||||
$[38] = isAllSelected;
|
|
||||||
$[39] = t10;
|
|
||||||
} else {
|
|
||||||
t10 = $[39];
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build navigable items (no separators)
|
||||||
|
const navigableItems: Array<{
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
action: () => void
|
||||||
|
isContinue?: boolean
|
||||||
|
isToggle?: boolean
|
||||||
|
isHeader?: boolean
|
||||||
|
}> = []
|
||||||
|
|
||||||
|
// Continue button
|
||||||
|
navigableItems.push({
|
||||||
|
id: 'continue',
|
||||||
|
label: 'Continue',
|
||||||
|
action: handleConfirm,
|
||||||
|
isContinue: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// All tools
|
||||||
|
navigableItems.push({
|
||||||
|
id: 'bucket-all',
|
||||||
|
label: `${isAllSelected ? figures.checkboxOn : figures.checkboxOff} All tools`,
|
||||||
|
action: () => {
|
||||||
|
const allToolNames = customAgentTools.map(t => t.name)
|
||||||
|
handleToggleTools(allToolNames, !isAllSelected)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create bucket menu items
|
||||||
|
const toolBuckets = getToolBuckets()
|
||||||
|
const bucketConfigs = [
|
||||||
|
{
|
||||||
|
id: 'bucket-readonly',
|
||||||
|
name: toolBuckets.READ_ONLY.name,
|
||||||
|
tools: toolsByBucket.readOnly,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bucket-edit',
|
||||||
|
name: toolBuckets.EDIT.name,
|
||||||
|
tools: toolsByBucket.edit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bucket-execution',
|
||||||
|
name: toolBuckets.EXECUTION.name,
|
||||||
|
tools: toolsByBucket.execution,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bucket-mcp',
|
||||||
|
name: toolBuckets.MCP.name,
|
||||||
|
tools: toolsByBucket.mcp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bucket-other',
|
||||||
|
name: toolBuckets.OTHER.name,
|
||||||
|
tools: toolsByBucket.other,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
bucketConfigs.forEach(({ id, name, tools: bucketTools }) => {
|
||||||
|
if (bucketTools.length === 0) return
|
||||||
|
|
||||||
|
const selected = count(bucketTools, t => selectedSet.has(t.name))
|
||||||
|
const isFullySelected = selected === bucketTools.length
|
||||||
|
|
||||||
navigableItems.push({
|
navigableItems.push({
|
||||||
id: "bucket-all",
|
id,
|
||||||
label: `${isAllSelected ? figures.checkboxOn : figures.checkboxOff} All tools`,
|
label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${name}`,
|
||||||
action: t10
|
action: createBucketToggleAction(bucketTools),
|
||||||
});
|
})
|
||||||
const toolBuckets_0 = getToolBuckets();
|
})
|
||||||
const bucketConfigs = [{
|
|
||||||
id: "bucket-readonly",
|
// Toggle button for individual tools
|
||||||
name: toolBuckets_0.READ_ONLY.name,
|
const toggleButtonIndex = navigableItems.length
|
||||||
tools: toolsByBucket.readOnly
|
navigableItems.push({
|
||||||
}, {
|
id: 'toggle-individual',
|
||||||
id: "bucket-edit",
|
label: showIndividualTools
|
||||||
name: toolBuckets_0.EDIT.name,
|
? 'Hide advanced options'
|
||||||
tools: toolsByBucket.edit
|
: 'Show advanced options',
|
||||||
}, {
|
action: () => {
|
||||||
id: "bucket-execution",
|
setShowIndividualTools(!showIndividualTools)
|
||||||
name: toolBuckets_0.EXECUTION.name,
|
// If hiding tools and focus is on an individual tool, move focus to toggle button
|
||||||
tools: toolsByBucket.execution
|
if (showIndividualTools && focusIndex > toggleButtonIndex) {
|
||||||
}, {
|
setFocusIndex(toggleButtonIndex)
|
||||||
id: "bucket-mcp",
|
|
||||||
name: toolBuckets_0.MCP.name,
|
|
||||||
tools: toolsByBucket.mcp
|
|
||||||
}, {
|
|
||||||
id: "bucket-other",
|
|
||||||
name: toolBuckets_0.OTHER.name,
|
|
||||||
tools: toolsByBucket.other
|
|
||||||
}];
|
|
||||||
bucketConfigs.forEach(t11 => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
name: name_1,
|
|
||||||
tools: bucketTools_0
|
|
||||||
} = t11;
|
|
||||||
if (bucketTools_0.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const selected_0 = count(bucketTools_0, (t_8: Tool) => selectedSet.has(t_8.name));
|
},
|
||||||
const isFullySelected = selected_0 === bucketTools_0.length;
|
isToggle: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Memoize MCP server buckets (must be outside conditional for hooks rules)
|
||||||
|
const mcpServerBuckets = useMemo(
|
||||||
|
() => getMcpServerBuckets(customAgentTools),
|
||||||
|
[customAgentTools],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Individual tools (only if expanded)
|
||||||
|
if (showIndividualTools) {
|
||||||
|
// Add MCP server buckets if any exist
|
||||||
|
if (mcpServerBuckets.length > 0) {
|
||||||
navigableItems.push({
|
navigableItems.push({
|
||||||
id,
|
id: 'mcp-servers-header',
|
||||||
label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${name_1}`,
|
label: 'MCP Servers:',
|
||||||
action: createBucketToggleAction(bucketTools_0)
|
action: () => {}, // No action - just a header
|
||||||
});
|
isHeader: true,
|
||||||
});
|
})
|
||||||
const toggleButtonIndex = navigableItems.length;
|
|
||||||
let t12;
|
mcpServerBuckets.forEach(({ serverName, tools: serverTools }) => {
|
||||||
if ($[40] !== focusIndex || $[41] !== showIndividualTools || $[42] !== toggleButtonIndex) {
|
const selected = count(serverTools, t => selectedSet.has(t.name))
|
||||||
t12 = () => {
|
const isFullySelected = selected === serverTools.length
|
||||||
setShowIndividualTools(!showIndividualTools);
|
|
||||||
if (showIndividualTools && focusIndex > toggleButtonIndex) {
|
navigableItems.push({
|
||||||
setFocusIndex(toggleButtonIndex);
|
id: `mcp-server-${serverName}`,
|
||||||
}
|
label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${serverName} (${serverTools.length} ${plural(serverTools.length, 'tool')})`,
|
||||||
};
|
action: () => {
|
||||||
$[40] = focusIndex;
|
const toolNames = serverTools.map(t => t.name)
|
||||||
$[41] = showIndividualTools;
|
handleToggleTools(toolNames, !isFullySelected)
|
||||||
$[42] = toggleButtonIndex;
|
},
|
||||||
$[43] = t12;
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add separator header before individual tools
|
||||||
|
navigableItems.push({
|
||||||
|
id: 'tools-header',
|
||||||
|
label: 'Individual Tools:',
|
||||||
|
action: () => {},
|
||||||
|
isHeader: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add individual tools
|
||||||
|
customAgentTools.forEach(tool => {
|
||||||
|
let displayName = tool.name
|
||||||
|
if (tool.name.startsWith('mcp__')) {
|
||||||
|
const mcpInfo = mcpInfoFromString(tool.name)
|
||||||
|
displayName = mcpInfo
|
||||||
|
? `${mcpInfo.toolName} (${mcpInfo.serverName})`
|
||||||
|
: tool.name
|
||||||
|
}
|
||||||
|
|
||||||
|
navigableItems.push({
|
||||||
|
id: `tool-${tool.name}`,
|
||||||
|
label: `${selectedSet.has(tool.name) ? figures.checkboxOn : figures.checkboxOff} ${displayName}`,
|
||||||
|
action: () => handleToggleTool(tool.name),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel()
|
||||||
} else {
|
} else {
|
||||||
t12 = $[43];
|
onComplete(initialTools)
|
||||||
}
|
}
|
||||||
navigableItems.push({
|
}, [onCancel, onComplete, initialTools])
|
||||||
id: "toggle-individual",
|
|
||||||
label: showIndividualTools ? "Hide advanced options" : "Show advanced options",
|
useKeybinding('confirm:no', handleCancel, { context: 'Confirmation' })
|
||||||
action: t12,
|
|
||||||
isToggle: true
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
});
|
if (e.key === 'return') {
|
||||||
const mcpServerBuckets = getMcpServerBuckets(customAgentTools);
|
e.preventDefault()
|
||||||
if (showIndividualTools) {
|
const item = navigableItems[focusIndex]
|
||||||
if (mcpServerBuckets.length > 0) {
|
if (item && !item.isHeader) {
|
||||||
navigableItems.push({
|
item.action()
|
||||||
id: "mcp-servers-header",
|
|
||||||
label: "MCP Servers:",
|
|
||||||
action: _temp6,
|
|
||||||
isHeader: true
|
|
||||||
});
|
|
||||||
mcpServerBuckets.forEach(t13 => {
|
|
||||||
const {
|
|
||||||
serverName,
|
|
||||||
tools: serverTools
|
|
||||||
} = t13;
|
|
||||||
const selected_1 = count(serverTools, t_9 => selectedSet.has(t_9.name));
|
|
||||||
const isFullySelected_0 = selected_1 === serverTools.length;
|
|
||||||
navigableItems.push({
|
|
||||||
id: `mcp-server-${serverName}`,
|
|
||||||
label: `${isFullySelected_0 ? figures.checkboxOn : figures.checkboxOff} ${serverName} (${serverTools.length} ${plural(serverTools.length, "tool")})`,
|
|
||||||
action: () => {
|
|
||||||
const toolNames_2 = serverTools.map(_temp7);
|
|
||||||
handleToggleTools(toolNames_2, !isFullySelected_0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
navigableItems.push({
|
|
||||||
id: "tools-header",
|
|
||||||
label: "Individual Tools:",
|
|
||||||
action: _temp8,
|
|
||||||
isHeader: true
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
customAgentTools.forEach(tool_0 => {
|
} else if (e.key === 'up') {
|
||||||
let displayName = tool_0.name;
|
e.preventDefault()
|
||||||
if (tool_0.name.startsWith("mcp__")) {
|
let newIndex = focusIndex - 1
|
||||||
const mcpInfo = mcpInfoFromString(tool_0.name);
|
// Skip headers when navigating up
|
||||||
displayName = mcpInfo ? `${mcpInfo.toolName} (${mcpInfo.serverName})` : tool_0.name;
|
while (newIndex > 0 && navigableItems[newIndex]?.isHeader) {
|
||||||
}
|
newIndex--
|
||||||
navigableItems.push({
|
}
|
||||||
id: `tool-${tool_0.name}`,
|
setFocusIndex(Math.max(0, newIndex))
|
||||||
label: `${selectedSet.has(tool_0.name) ? figures.checkboxOn : figures.checkboxOff} ${displayName}`,
|
} else if (e.key === 'down') {
|
||||||
action: () => handleToggleTool(tool_0.name)
|
e.preventDefault()
|
||||||
});
|
let newIndex = focusIndex + 1
|
||||||
});
|
// Skip headers when navigating down
|
||||||
|
while (
|
||||||
|
newIndex < navigableItems.length - 1 &&
|
||||||
|
navigableItems[newIndex]?.isHeader
|
||||||
|
) {
|
||||||
|
newIndex++
|
||||||
|
}
|
||||||
|
setFocusIndex(Math.min(navigableItems.length - 1, newIndex))
|
||||||
}
|
}
|
||||||
$[24] = createBucketToggleAction;
|
|
||||||
$[25] = customAgentTools;
|
|
||||||
$[26] = focusIndex;
|
|
||||||
$[27] = handleConfirm;
|
|
||||||
$[28] = isAllSelected;
|
|
||||||
$[29] = selectedSet;
|
|
||||||
$[30] = showIndividualTools;
|
|
||||||
$[31] = toolsByBucket.edit;
|
|
||||||
$[32] = toolsByBucket.execution;
|
|
||||||
$[33] = toolsByBucket.mcp;
|
|
||||||
$[34] = toolsByBucket.other;
|
|
||||||
$[35] = toolsByBucket.readOnly;
|
|
||||||
$[36] = navigableItems;
|
|
||||||
} else {
|
|
||||||
navigableItems = $[36];
|
|
||||||
}
|
}
|
||||||
let t10;
|
|
||||||
if ($[44] !== initialTools || $[45] !== onCancel || $[46] !== onComplete) {
|
return (
|
||||||
t10 = () => {
|
<Box
|
||||||
if (onCancel) {
|
flexDirection="column"
|
||||||
onCancel();
|
marginTop={1}
|
||||||
} else {
|
tabIndex={0}
|
||||||
onComplete(initialTools);
|
autoFocus
|
||||||
}
|
onKeyDown={handleKeyDown}
|
||||||
};
|
>
|
||||||
$[44] = initialTools;
|
{/* Render Continue button */}
|
||||||
$[45] = onCancel;
|
<Text
|
||||||
$[46] = onComplete;
|
color={focusIndex === 0 ? 'suggestion' : undefined}
|
||||||
$[47] = t10;
|
bold={focusIndex === 0}
|
||||||
} else {
|
>
|
||||||
t10 = $[47];
|
{focusIndex === 0 ? `${figures.pointer} ` : ' '}[ Continue ]
|
||||||
}
|
</Text>
|
||||||
const handleCancel = t10;
|
|
||||||
let t11;
|
{/* Separator */}
|
||||||
if ($[48] === Symbol.for("react.memo_cache_sentinel")) {
|
<Divider width={40} />
|
||||||
t11 = {
|
|
||||||
context: "Confirmation"
|
{/* Render all navigable items except Continue (which is at index 0) */}
|
||||||
};
|
{navigableItems.slice(1).map((item, index) => {
|
||||||
$[48] = t11;
|
const isCurrentlyFocused = index + 1 === focusIndex
|
||||||
} else {
|
const isToggleButton = item.isToggle
|
||||||
t11 = $[48];
|
const isHeader = item.isHeader
|
||||||
}
|
|
||||||
useKeybinding("confirm:no", handleCancel, t11);
|
return (
|
||||||
let t12;
|
<React.Fragment key={item.id}>
|
||||||
if ($[49] !== focusIndex || $[50] !== navigableItems) {
|
{/* Add separator before toggle button */}
|
||||||
t12 = e => {
|
{isToggleButton && <Divider width={40} />}
|
||||||
if (e.key === "return") {
|
|
||||||
e.preventDefault();
|
{/* Add margin before headers */}
|
||||||
const item = navigableItems[focusIndex];
|
{isHeader && index > 0 && <Box marginTop={1} />}
|
||||||
if (item && !item.isHeader) {
|
|
||||||
item.action();
|
<Text
|
||||||
}
|
color={
|
||||||
} else {
|
isHeader
|
||||||
if (e.key === "up") {
|
? undefined
|
||||||
e.preventDefault();
|
: isCurrentlyFocused
|
||||||
let newIndex = focusIndex - 1;
|
? 'suggestion'
|
||||||
while (newIndex > 0 && navigableItems[newIndex]?.isHeader) {
|
: undefined
|
||||||
newIndex--;
|
}
|
||||||
}
|
dimColor={isHeader}
|
||||||
setFocusIndex(Math.max(0, newIndex));
|
bold={isToggleButton && isCurrentlyFocused}
|
||||||
} else {
|
>
|
||||||
if (e.key === "down") {
|
{isHeader
|
||||||
e.preventDefault();
|
? ''
|
||||||
let newIndex_0 = focusIndex + 1;
|
: isCurrentlyFocused
|
||||||
while (newIndex_0 < navigableItems.length - 1 && navigableItems[newIndex_0]?.isHeader) {
|
? `${figures.pointer} `
|
||||||
newIndex_0++;
|
: ' '}
|
||||||
}
|
{isToggleButton ? `[ ${item.label} ]` : item.label}
|
||||||
setFocusIndex(Math.min(navigableItems.length - 1, newIndex_0));
|
</Text>
|
||||||
}
|
</React.Fragment>
|
||||||
}
|
)
|
||||||
}
|
})}
|
||||||
};
|
|
||||||
$[49] = focusIndex;
|
<Box marginTop={1} flexDirection="column">
|
||||||
$[50] = navigableItems;
|
<Text dimColor>
|
||||||
$[51] = t12;
|
{isAllSelected
|
||||||
} else {
|
? 'All tools selected'
|
||||||
t12 = $[51];
|
: `${selectedSet.size} of ${customAgentTools.length} tools selected`}
|
||||||
}
|
</Text>
|
||||||
const handleKeyDown = t12;
|
</Box>
|
||||||
const t13 = focusIndex === 0 ? "suggestion" : undefined;
|
</Box>
|
||||||
const t14 = focusIndex === 0;
|
)
|
||||||
const t15 = focusIndex === 0 ? `${figures.pointer} ` : " ";
|
|
||||||
let t16;
|
|
||||||
if ($[52] !== t13 || $[53] !== t14 || $[54] !== t15) {
|
|
||||||
t16 = <Text color={t13} bold={t14}>{t15}[ Continue ]</Text>;
|
|
||||||
$[52] = t13;
|
|
||||||
$[53] = t14;
|
|
||||||
$[54] = t15;
|
|
||||||
$[55] = t16;
|
|
||||||
} else {
|
|
||||||
t16 = $[55];
|
|
||||||
}
|
|
||||||
let t17;
|
|
||||||
if ($[56] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t17 = <Divider width={40} />;
|
|
||||||
$[56] = t17;
|
|
||||||
} else {
|
|
||||||
t17 = $[56];
|
|
||||||
}
|
|
||||||
let t18;
|
|
||||||
if ($[57] !== navigableItems) {
|
|
||||||
t18 = navigableItems.slice(1);
|
|
||||||
$[57] = navigableItems;
|
|
||||||
$[58] = t18;
|
|
||||||
} else {
|
|
||||||
t18 = $[58];
|
|
||||||
}
|
|
||||||
let t19;
|
|
||||||
if ($[59] !== focusIndex || $[60] !== t18) {
|
|
||||||
t19 = t18.map((item_0, index) => {
|
|
||||||
const isCurrentlyFocused = index + 1 === focusIndex;
|
|
||||||
const isToggleButton = item_0.isToggle;
|
|
||||||
const isHeader = item_0.isHeader;
|
|
||||||
return <React.Fragment key={item_0.id}>{isToggleButton && <Divider width={40} />}{isHeader && index > 0 && <Box marginTop={1} />}<Text color={isHeader ? undefined : isCurrentlyFocused ? "suggestion" : undefined} dimColor={isHeader} bold={isToggleButton && isCurrentlyFocused}>{isHeader ? "" : isCurrentlyFocused ? `${figures.pointer} ` : " "}{isToggleButton ? `[ ${item_0.label} ]` : item_0.label}</Text></React.Fragment>;
|
|
||||||
});
|
|
||||||
$[59] = focusIndex;
|
|
||||||
$[60] = t18;
|
|
||||||
$[61] = t19;
|
|
||||||
} else {
|
|
||||||
t19 = $[61];
|
|
||||||
}
|
|
||||||
const t20 = isAllSelected ? "All tools selected" : `${selectedSet.size} of ${customAgentTools.length} tools selected`;
|
|
||||||
let t21;
|
|
||||||
if ($[62] !== t20) {
|
|
||||||
t21 = <Box marginTop={1} flexDirection="column"><Text dimColor={true}>{t20}</Text></Box>;
|
|
||||||
$[62] = t20;
|
|
||||||
$[63] = t21;
|
|
||||||
} else {
|
|
||||||
t21 = $[63];
|
|
||||||
}
|
|
||||||
let t22;
|
|
||||||
if ($[64] !== handleKeyDown || $[65] !== t16 || $[66] !== t19 || $[67] !== t21) {
|
|
||||||
t22 = <Box flexDirection="column" marginTop={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t16}{t17}{t19}{t21}</Box>;
|
|
||||||
$[64] = handleKeyDown;
|
|
||||||
$[65] = t16;
|
|
||||||
$[66] = t19;
|
|
||||||
$[67] = t21;
|
|
||||||
$[68] = t22;
|
|
||||||
} else {
|
|
||||||
t22 = $[68];
|
|
||||||
}
|
|
||||||
return t22;
|
|
||||||
}
|
|
||||||
function _temp8() {}
|
|
||||||
function _temp7(t_10) {
|
|
||||||
return t_10.name;
|
|
||||||
}
|
|
||||||
function _temp6() {}
|
|
||||||
function _temp5(t_7) {
|
|
||||||
return t_7.name;
|
|
||||||
}
|
|
||||||
function _temp4(t_6) {
|
|
||||||
return t_6.name;
|
|
||||||
}
|
|
||||||
function _temp3(t_4) {
|
|
||||||
return t_4.name;
|
|
||||||
}
|
|
||||||
function _temp2(t_0) {
|
|
||||||
return t_0.name;
|
|
||||||
}
|
|
||||||
function _temp(t) {
|
|
||||||
return t.name;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,96 +1,68 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { type ReactNode } from 'react'
|
||||||
import React, { type ReactNode } from 'react';
|
import { isAutoMemoryEnabled } from '../../../memdir/paths.js'
|
||||||
import { isAutoMemoryEnabled } from '../../../memdir/paths.js';
|
import type { Tools } from '../../../Tool.js'
|
||||||
import type { Tools } from '../../../Tool.js';
|
import type { AgentDefinition } from '../../../tools/AgentTool/loadAgentsDir.js'
|
||||||
import type { AgentDefinition } from '../../../tools/AgentTool/loadAgentsDir.js';
|
import { WizardProvider } from '../../wizard/index.js'
|
||||||
import { WizardProvider } from '../../wizard/index.js';
|
import type { WizardStepComponent } from '../../wizard/types.js'
|
||||||
import type { WizardStepComponent } from '../../wizard/types.js';
|
import type { AgentWizardData } from './types.js'
|
||||||
import type { AgentWizardData } from './types.js';
|
import { ColorStep } from './wizard-steps/ColorStep.js'
|
||||||
import { ColorStep } from './wizard-steps/ColorStep.js';
|
import { ConfirmStepWrapper } from './wizard-steps/ConfirmStepWrapper.js'
|
||||||
import { ConfirmStepWrapper } from './wizard-steps/ConfirmStepWrapper.js';
|
import { DescriptionStep } from './wizard-steps/DescriptionStep.js'
|
||||||
import { DescriptionStep } from './wizard-steps/DescriptionStep.js';
|
import { GenerateStep } from './wizard-steps/GenerateStep.js'
|
||||||
import { GenerateStep } from './wizard-steps/GenerateStep.js';
|
import { LocationStep } from './wizard-steps/LocationStep.js'
|
||||||
import { LocationStep } from './wizard-steps/LocationStep.js';
|
import { MemoryStep } from './wizard-steps/MemoryStep.js'
|
||||||
import { MemoryStep } from './wizard-steps/MemoryStep.js';
|
import { MethodStep } from './wizard-steps/MethodStep.js'
|
||||||
import { MethodStep } from './wizard-steps/MethodStep.js';
|
import { ModelStep } from './wizard-steps/ModelStep.js'
|
||||||
import { ModelStep } from './wizard-steps/ModelStep.js';
|
import { PromptStep } from './wizard-steps/PromptStep.js'
|
||||||
import { PromptStep } from './wizard-steps/PromptStep.js';
|
import { ToolsStep } from './wizard-steps/ToolsStep.js'
|
||||||
import { ToolsStep } from './wizard-steps/ToolsStep.js';
|
import { TypeStep } from './wizard-steps/TypeStep.js'
|
||||||
import { TypeStep } from './wizard-steps/TypeStep.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tools: Tools;
|
tools: Tools
|
||||||
existingAgents: AgentDefinition[];
|
existingAgents: AgentDefinition[]
|
||||||
onComplete: (message: string) => void;
|
onComplete: (message: string) => void
|
||||||
onCancel: () => void;
|
onCancel: () => void
|
||||||
};
|
}
|
||||||
export function CreateAgentWizard(t0) {
|
|
||||||
const $ = _c(17);
|
export function CreateAgentWizard({
|
||||||
const {
|
tools,
|
||||||
tools,
|
existingAgents,
|
||||||
existingAgents,
|
onComplete,
|
||||||
onComplete,
|
onCancel,
|
||||||
onCancel
|
}: Props): ReactNode {
|
||||||
} = t0;
|
// Create step components with props
|
||||||
let t1;
|
const steps: WizardStepComponent<AgentWizardData>[] = [
|
||||||
if ($[0] !== existingAgents) {
|
LocationStep, // 0
|
||||||
t1 = () => <TypeStep existingAgents={existingAgents} />;
|
MethodStep, // 1
|
||||||
$[0] = existingAgents;
|
GenerateStep, // 2
|
||||||
$[1] = t1;
|
() => <TypeStep existingAgents={existingAgents} />, // 3
|
||||||
} else {
|
PromptStep, // 4
|
||||||
t1 = $[1];
|
DescriptionStep, // 5
|
||||||
}
|
() => <ToolsStep tools={tools} />, // 6
|
||||||
let t2;
|
ModelStep, // 7
|
||||||
if ($[2] !== tools) {
|
ColorStep, // 8
|
||||||
t2 = () => <ToolsStep tools={tools} />;
|
// MemoryStep is conditionally included based on GrowthBook gate
|
||||||
$[2] = tools;
|
...(isAutoMemoryEnabled() ? [MemoryStep] : []),
|
||||||
$[3] = t2;
|
() => (
|
||||||
} else {
|
<ConfirmStepWrapper
|
||||||
t2 = $[3];
|
tools={tools}
|
||||||
}
|
existingAgents={existingAgents}
|
||||||
let t3;
|
onComplete={onComplete}
|
||||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
/>
|
||||||
t3 = isAutoMemoryEnabled() ? [MemoryStep] : [];
|
),
|
||||||
$[4] = t3;
|
]
|
||||||
} else {
|
|
||||||
t3 = $[4];
|
return (
|
||||||
}
|
<WizardProvider<AgentWizardData>
|
||||||
let t4;
|
steps={steps}
|
||||||
if ($[5] !== existingAgents || $[6] !== onComplete || $[7] !== tools) {
|
initialData={{}}
|
||||||
t4 = () => <ConfirmStepWrapper tools={tools} existingAgents={existingAgents} onComplete={onComplete} />;
|
onComplete={() => {
|
||||||
$[5] = existingAgents;
|
// Wizard completion is handled by ConfirmStepWrapper
|
||||||
$[6] = onComplete;
|
// which calls onComplete with the appropriate message
|
||||||
$[7] = tools;
|
}}
|
||||||
$[8] = t4;
|
onCancel={onCancel}
|
||||||
} else {
|
title="Create new agent"
|
||||||
t4 = $[8];
|
showStepCounter={false}
|
||||||
}
|
/>
|
||||||
let t5;
|
)
|
||||||
if ($[9] !== t1 || $[10] !== t2 || $[11] !== t4) {
|
|
||||||
t5 = [LocationStep, MethodStep, GenerateStep, t1, PromptStep, DescriptionStep, t2, ModelStep, ColorStep, ...t3, t4];
|
|
||||||
$[9] = t1;
|
|
||||||
$[10] = t2;
|
|
||||||
$[11] = t4;
|
|
||||||
$[12] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[12];
|
|
||||||
}
|
|
||||||
const steps = t5;
|
|
||||||
let t6;
|
|
||||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t6 = {};
|
|
||||||
$[13] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[13];
|
|
||||||
}
|
|
||||||
let t7;
|
|
||||||
if ($[14] !== onCancel || $[15] !== steps) {
|
|
||||||
t7 = <WizardProvider steps={steps} initialData={t6} onComplete={_temp} onCancel={onCancel} title="Create new agent" showStepCounter={false} />;
|
|
||||||
$[14] = onCancel;
|
|
||||||
$[15] = steps;
|
|
||||||
$[16] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[16];
|
|
||||||
}
|
|
||||||
return t7;
|
|
||||||
}
|
}
|
||||||
function _temp() {}
|
|
||||||
|
|||||||
@@ -1,83 +1,64 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { type ReactNode } from 'react'
|
||||||
import React, { type ReactNode } from 'react';
|
import { Box } from '../../../../ink.js'
|
||||||
import { Box } from '../../../../ink.js';
|
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
|
||||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
|
import type { AgentColorName } from '../../../../tools/AgentTool/agentColorManager.js'
|
||||||
import type { AgentColorName } from '../../../../tools/AgentTool/agentColorManager.js';
|
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
import { Byline } from '../../../design-system/Byline.js'
|
||||||
import { Byline } from '../../../design-system/Byline.js';
|
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
import { useWizard } from '../../../wizard/index.js'
|
||||||
import { useWizard } from '../../../wizard/index.js';
|
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
import { ColorPicker } from '../../ColorPicker.js'
|
||||||
import { ColorPicker } from '../../ColorPicker.js';
|
import type { AgentWizardData } from '../types.js'
|
||||||
import type { AgentWizardData } from '../types.js';
|
|
||||||
export function ColorStep() {
|
export function ColorStep(): ReactNode {
|
||||||
const $ = _c(14);
|
const { goNext, goBack, updateWizardData, wizardData } =
|
||||||
const {
|
useWizard<AgentWizardData>()
|
||||||
goNext,
|
|
||||||
goBack,
|
// Handle escape key - ColorPicker handles its own escape internally
|
||||||
updateWizardData,
|
useKeybinding('confirm:no', goBack, { context: 'Confirmation' })
|
||||||
wizardData
|
|
||||||
} = useWizard();
|
const handleConfirm = (color?: string): void => {
|
||||||
let t0;
|
updateWizardData({
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
selectedColor: color,
|
||||||
t0 = {
|
// Prepare final agent for confirmation
|
||||||
context: "Confirmation"
|
finalAgent: {
|
||||||
};
|
agentType: wizardData.agentType!,
|
||||||
$[0] = t0;
|
whenToUse: wizardData.whenToUse!,
|
||||||
} else {
|
getSystemPrompt: () => wizardData.systemPrompt!,
|
||||||
t0 = $[0];
|
tools: wizardData.selectedTools,
|
||||||
|
...(wizardData.selectedModel
|
||||||
|
? { model: wizardData.selectedModel }
|
||||||
|
: {}),
|
||||||
|
...(color ? { color: color as AgentColorName } : {}),
|
||||||
|
source: wizardData.location!,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
goNext()
|
||||||
}
|
}
|
||||||
useKeybinding("confirm:no", goBack, t0);
|
|
||||||
let t1;
|
return (
|
||||||
if ($[1] !== goNext || $[2] !== updateWizardData || $[3] !== wizardData.agentType || $[4] !== wizardData.location || $[5] !== wizardData.selectedModel || $[6] !== wizardData.selectedTools || $[7] !== wizardData.systemPrompt || $[8] !== wizardData.whenToUse) {
|
<WizardDialogLayout
|
||||||
t1 = color => {
|
subtitle="Choose background color"
|
||||||
updateWizardData({
|
footerText={
|
||||||
selectedColor: color,
|
<Byline>
|
||||||
finalAgent: {
|
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||||
agentType: wizardData.agentType,
|
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
||||||
whenToUse: wizardData.whenToUse,
|
<ConfigurableShortcutHint
|
||||||
getSystemPrompt: () => wizardData.systemPrompt,
|
action="confirm:no"
|
||||||
tools: wizardData.selectedTools,
|
context="Confirmation"
|
||||||
...(wizardData.selectedModel ? {
|
fallback="Esc"
|
||||||
model: wizardData.selectedModel
|
description="go back"
|
||||||
} : {}),
|
/>
|
||||||
...(color ? {
|
</Byline>
|
||||||
color: color as AgentColorName
|
}
|
||||||
} : {}),
|
>
|
||||||
source: wizardData.location
|
<Box>
|
||||||
}
|
<ColorPicker
|
||||||
});
|
agentName={wizardData.agentType || 'agent'}
|
||||||
goNext();
|
currentColor="automatic"
|
||||||
};
|
onConfirm={handleConfirm}
|
||||||
$[1] = goNext;
|
/>
|
||||||
$[2] = updateWizardData;
|
</Box>
|
||||||
$[3] = wizardData.agentType;
|
</WizardDialogLayout>
|
||||||
$[4] = wizardData.location;
|
)
|
||||||
$[5] = wizardData.selectedModel;
|
|
||||||
$[6] = wizardData.selectedTools;
|
|
||||||
$[7] = wizardData.systemPrompt;
|
|
||||||
$[8] = wizardData.whenToUse;
|
|
||||||
$[9] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[9];
|
|
||||||
}
|
|
||||||
const handleConfirm = t1;
|
|
||||||
let t2;
|
|
||||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t2 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
|
|
||||||
$[10] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[10];
|
|
||||||
}
|
|
||||||
const t3 = wizardData.agentType || "agent";
|
|
||||||
let t4;
|
|
||||||
if ($[11] !== handleConfirm || $[12] !== t3) {
|
|
||||||
t4 = <WizardDialogLayout subtitle="Choose background color" footerText={t2}><Box><ColorPicker agentName={t3} currentColor="automatic" onConfirm={handleConfirm} /></Box></WizardDialogLayout>;
|
|
||||||
$[11] = handleConfirm;
|
|
||||||
$[12] = t3;
|
|
||||||
$[13] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[13];
|
|
||||||
}
|
|
||||||
return t4;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,377 +1,168 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { type ReactNode } from 'react'
|
||||||
import React, { type ReactNode } from 'react';
|
import type { KeyboardEvent } from '../../../../ink/events/keyboard-event.js'
|
||||||
import type { KeyboardEvent } from '../../../../ink/events/keyboard-event.js';
|
import { Box, Text } from '../../../../ink.js'
|
||||||
import { Box, Text } from '../../../../ink.js';
|
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
|
||||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
|
import { isAutoMemoryEnabled } from '../../../../memdir/paths.js'
|
||||||
import { isAutoMemoryEnabled } from '../../../../memdir/paths.js';
|
import type { Tools } from '../../../../Tool.js'
|
||||||
import type { Tools } from '../../../../Tool.js';
|
import { getMemoryScopeDisplay } from '../../../../tools/AgentTool/agentMemory.js'
|
||||||
import { getMemoryScopeDisplay } from '../../../../tools/AgentTool/agentMemory.js';
|
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'
|
||||||
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js';
|
import { truncateToWidth } from '../../../../utils/format.js'
|
||||||
import { truncateToWidth } from '../../../../utils/format.js';
|
import { getAgentModelDisplay } from '../../../../utils/model/agent.js'
|
||||||
import { getAgentModelDisplay } from '../../../../utils/model/agent.js';
|
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
import { Byline } from '../../../design-system/Byline.js'
|
||||||
import { Byline } from '../../../design-system/Byline.js';
|
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
import { useWizard } from '../../../wizard/index.js'
|
||||||
import { useWizard } from '../../../wizard/index.js';
|
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
import { getNewRelativeAgentFilePath } from '../../agentFileUtils.js'
|
||||||
import { getNewRelativeAgentFilePath } from '../../agentFileUtils.js';
|
import { validateAgent } from '../../validateAgent.js'
|
||||||
import { validateAgent } from '../../validateAgent.js';
|
import type { AgentWizardData } from '../types.js'
|
||||||
import type { AgentWizardData } from '../types.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tools: Tools;
|
tools: Tools
|
||||||
existingAgents: AgentDefinition[];
|
existingAgents: AgentDefinition[]
|
||||||
onSave: () => void;
|
onSave: () => void
|
||||||
onSaveAndEdit: () => void;
|
onSaveAndEdit: () => void
|
||||||
error?: string | null;
|
error?: string | null
|
||||||
};
|
}
|
||||||
export function ConfirmStep(t0) {
|
|
||||||
const $ = _c(88);
|
export function ConfirmStep({
|
||||||
const {
|
tools,
|
||||||
tools,
|
existingAgents,
|
||||||
existingAgents,
|
onSave,
|
||||||
onSave,
|
onSaveAndEdit,
|
||||||
onSaveAndEdit,
|
error,
|
||||||
error
|
}: Props): ReactNode {
|
||||||
} = t0;
|
const { goBack, wizardData } = useWizard<AgentWizardData>()
|
||||||
const {
|
|
||||||
goBack,
|
useKeybinding('confirm:no', goBack, { context: 'Confirmation' })
|
||||||
wizardData
|
|
||||||
} = useWizard();
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
let t1;
|
if (e.key === 's' || e.key === 'return') {
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
e.preventDefault()
|
||||||
t1 = {
|
onSave()
|
||||||
context: "Confirmation"
|
} else if (e.key === 'e') {
|
||||||
};
|
e.preventDefault()
|
||||||
$[0] = t1;
|
onSaveAndEdit()
|
||||||
} else {
|
}
|
||||||
t1 = $[0];
|
|
||||||
}
|
}
|
||||||
useKeybinding("confirm:no", goBack, t1);
|
|
||||||
let t2;
|
const agent = wizardData.finalAgent!
|
||||||
if ($[1] !== onSave || $[2] !== onSaveAndEdit) {
|
const validation = validateAgent(agent, tools, existingAgents)
|
||||||
t2 = e => {
|
|
||||||
if (e.key === "s" || e.key === "return") {
|
const systemPromptPreview = truncateToWidth(agent.getSystemPrompt(), 240)
|
||||||
e.preventDefault();
|
const whenToUsePreview = truncateToWidth(agent.whenToUse, 240)
|
||||||
onSave();
|
|
||||||
} else {
|
const getToolsDisplay = (toolNames: string[] | undefined): string => {
|
||||||
if (e.key === "e") {
|
// undefined means "all tools" per PR semantic
|
||||||
e.preventDefault();
|
if (toolNames === undefined) return 'All tools'
|
||||||
onSaveAndEdit();
|
if (toolNames.length === 0) return 'None'
|
||||||
}
|
if (toolNames.length === 1) return toolNames[0] || 'None'
|
||||||
|
if (toolNames.length === 2) return toolNames.join(' and ')
|
||||||
|
return `${toolNames.slice(0, -1).join(', ')}, and ${toolNames[toolNames.length - 1]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute memory display outside JSX
|
||||||
|
const memoryDisplayElement = isAutoMemoryEnabled() ? (
|
||||||
|
<Text>
|
||||||
|
<Text bold>Memory</Text>: {getMemoryScopeDisplay(agent.memory)}
|
||||||
|
</Text>
|
||||||
|
) : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WizardDialogLayout
|
||||||
|
subtitle="Confirm and save"
|
||||||
|
footerText={
|
||||||
|
<Byline>
|
||||||
|
<KeyboardShortcutHint shortcut="s/Enter" action="save" />
|
||||||
|
<KeyboardShortcutHint shortcut="e" action="edit in your editor" />
|
||||||
|
<ConfigurableShortcutHint
|
||||||
|
action="confirm:no"
|
||||||
|
context="Confirmation"
|
||||||
|
fallback="Esc"
|
||||||
|
description="cancel"
|
||||||
|
/>
|
||||||
|
</Byline>
|
||||||
}
|
}
|
||||||
};
|
>
|
||||||
$[1] = onSave;
|
<Box
|
||||||
$[2] = onSaveAndEdit;
|
flexDirection="column"
|
||||||
$[3] = t2;
|
tabIndex={0}
|
||||||
} else {
|
autoFocus
|
||||||
t2 = $[3];
|
onKeyDown={handleKeyDown}
|
||||||
}
|
>
|
||||||
const handleKeyDown = t2;
|
<Text>
|
||||||
const agent = wizardData.finalAgent;
|
<Text bold>Name</Text>: {agent.agentType}
|
||||||
let T0;
|
</Text>
|
||||||
let T1;
|
<Text>
|
||||||
let t10;
|
<Text bold>Location</Text>:{' '}
|
||||||
let t11;
|
{getNewRelativeAgentFilePath({
|
||||||
let t12;
|
source: wizardData.location!,
|
||||||
let t13;
|
agentType: agent.agentType,
|
||||||
let t14;
|
})}
|
||||||
let t15;
|
</Text>
|
||||||
let t16;
|
<Text>
|
||||||
let t17;
|
<Text bold>Tools</Text>: {getToolsDisplay(agent.tools)}
|
||||||
let t18;
|
</Text>
|
||||||
let t19;
|
<Text>
|
||||||
let t3;
|
<Text bold>Model</Text>: {getAgentModelDisplay(agent.model)}
|
||||||
let t4;
|
</Text>
|
||||||
let t5;
|
{memoryDisplayElement}
|
||||||
let t6;
|
|
||||||
let t7;
|
<Box marginTop={1}>
|
||||||
let t8;
|
<Text>
|
||||||
let t9;
|
<Text bold>Description</Text> (tells Claude when to use this agent):
|
||||||
if ($[4] !== agent || $[5] !== existingAgents || $[6] !== handleKeyDown || $[7] !== tools || $[8] !== wizardData.location) {
|
</Text>
|
||||||
const validation = validateAgent(agent, tools, existingAgents);
|
</Box>
|
||||||
let t20;
|
<Box marginLeft={2} marginTop={1}>
|
||||||
if ($[28] !== agent) {
|
<Text>{whenToUsePreview}</Text>
|
||||||
t20 = truncateToWidth(agent.getSystemPrompt(), 240);
|
</Box>
|
||||||
$[28] = agent;
|
|
||||||
$[29] = t20;
|
<Box marginTop={1}>
|
||||||
} else {
|
<Text>
|
||||||
t20 = $[29];
|
<Text bold>System prompt</Text>:
|
||||||
}
|
</Text>
|
||||||
const systemPromptPreview = t20;
|
</Box>
|
||||||
let t21;
|
<Box marginLeft={2} marginTop={1}>
|
||||||
if ($[30] !== agent.whenToUse) {
|
<Text>{systemPromptPreview}</Text>
|
||||||
t21 = truncateToWidth(agent.whenToUse, 240);
|
</Box>
|
||||||
$[30] = agent.whenToUse;
|
|
||||||
$[31] = t21;
|
{validation.warnings.length > 0 && (
|
||||||
} else {
|
<Box marginTop={1} flexDirection="column">
|
||||||
t21 = $[31];
|
<Text color="warning">Warnings:</Text>
|
||||||
}
|
{validation.warnings.map((warning, i) => (
|
||||||
const whenToUsePreview = t21;
|
<Text key={i} dimColor>
|
||||||
const getToolsDisplay = _temp;
|
{' '}
|
||||||
let t22;
|
• {warning}
|
||||||
if ($[32] !== agent.memory) {
|
</Text>
|
||||||
t22 = isAutoMemoryEnabled() ? <Text><Text bold={true}>Memory</Text>: {getMemoryScopeDisplay(agent.memory)}</Text> : null;
|
))}
|
||||||
$[32] = agent.memory;
|
</Box>
|
||||||
$[33] = t22;
|
)}
|
||||||
} else {
|
|
||||||
t22 = $[33];
|
{validation.errors.length > 0 && (
|
||||||
}
|
<Box marginTop={1} flexDirection="column">
|
||||||
const memoryDisplayElement = t22;
|
<Text color="error">Errors:</Text>
|
||||||
T1 = WizardDialogLayout;
|
{validation.errors.map((err, i) => (
|
||||||
t18 = "Confirm and save";
|
<Text key={i} color="error">
|
||||||
if ($[34] === Symbol.for("react.memo_cache_sentinel")) {
|
{' '}
|
||||||
t19 = <Byline><KeyboardShortcutHint shortcut="s/Enter" action="save" /><KeyboardShortcutHint shortcut="e" action="edit in your editor" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /></Byline>;
|
• {err}
|
||||||
$[34] = t19;
|
</Text>
|
||||||
} else {
|
))}
|
||||||
t19 = $[34];
|
</Box>
|
||||||
}
|
)}
|
||||||
T0 = Box;
|
|
||||||
t3 = "column";
|
{error && (
|
||||||
t4 = 0;
|
<Box marginTop={1}>
|
||||||
t5 = true;
|
<Text color="error">{error}</Text>
|
||||||
t6 = handleKeyDown;
|
</Box>
|
||||||
let t23;
|
)}
|
||||||
if ($[35] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t23 = <Text bold={true}>Name</Text>;
|
<Box marginTop={2}>
|
||||||
$[35] = t23;
|
<Text color="success">
|
||||||
} else {
|
Press <Text bold>s</Text> or <Text bold>Enter</Text> to save,{' '}
|
||||||
t23 = $[35];
|
<Text bold>e</Text> to save and edit
|
||||||
}
|
</Text>
|
||||||
if ($[36] !== agent.agentType) {
|
</Box>
|
||||||
t7 = <Text>{t23}: {agent.agentType}</Text>;
|
</Box>
|
||||||
$[36] = agent.agentType;
|
</WizardDialogLayout>
|
||||||
$[37] = t7;
|
)
|
||||||
} else {
|
|
||||||
t7 = $[37];
|
|
||||||
}
|
|
||||||
let t24;
|
|
||||||
if ($[38] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t24 = <Text bold={true}>Location</Text>;
|
|
||||||
$[38] = t24;
|
|
||||||
} else {
|
|
||||||
t24 = $[38];
|
|
||||||
}
|
|
||||||
let t25;
|
|
||||||
if ($[39] !== agent.agentType || $[40] !== wizardData.location) {
|
|
||||||
t25 = getNewRelativeAgentFilePath({
|
|
||||||
source: wizardData.location,
|
|
||||||
agentType: agent.agentType
|
|
||||||
});
|
|
||||||
$[39] = agent.agentType;
|
|
||||||
$[40] = wizardData.location;
|
|
||||||
$[41] = t25;
|
|
||||||
} else {
|
|
||||||
t25 = $[41];
|
|
||||||
}
|
|
||||||
if ($[42] !== t25) {
|
|
||||||
t8 = <Text>{t24}:{" "}{t25}</Text>;
|
|
||||||
$[42] = t25;
|
|
||||||
$[43] = t8;
|
|
||||||
} else {
|
|
||||||
t8 = $[43];
|
|
||||||
}
|
|
||||||
let t26;
|
|
||||||
if ($[44] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t26 = <Text bold={true}>Tools</Text>;
|
|
||||||
$[44] = t26;
|
|
||||||
} else {
|
|
||||||
t26 = $[44];
|
|
||||||
}
|
|
||||||
let t27;
|
|
||||||
if ($[45] !== agent.tools) {
|
|
||||||
t27 = getToolsDisplay(agent.tools);
|
|
||||||
$[45] = agent.tools;
|
|
||||||
$[46] = t27;
|
|
||||||
} else {
|
|
||||||
t27 = $[46];
|
|
||||||
}
|
|
||||||
if ($[47] !== t27) {
|
|
||||||
t9 = <Text>{t26}: {t27}</Text>;
|
|
||||||
$[47] = t27;
|
|
||||||
$[48] = t9;
|
|
||||||
} else {
|
|
||||||
t9 = $[48];
|
|
||||||
}
|
|
||||||
let t28;
|
|
||||||
if ($[49] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t28 = <Text bold={true}>Model</Text>;
|
|
||||||
$[49] = t28;
|
|
||||||
} else {
|
|
||||||
t28 = $[49];
|
|
||||||
}
|
|
||||||
let t29;
|
|
||||||
if ($[50] !== agent.model) {
|
|
||||||
t29 = getAgentModelDisplay(agent.model);
|
|
||||||
$[50] = agent.model;
|
|
||||||
$[51] = t29;
|
|
||||||
} else {
|
|
||||||
t29 = $[51];
|
|
||||||
}
|
|
||||||
if ($[52] !== t29) {
|
|
||||||
t10 = <Text>{t28}: {t29}</Text>;
|
|
||||||
$[52] = t29;
|
|
||||||
$[53] = t10;
|
|
||||||
} else {
|
|
||||||
t10 = $[53];
|
|
||||||
}
|
|
||||||
t11 = memoryDisplayElement;
|
|
||||||
if ($[54] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t12 = <Box marginTop={1}><Text><Text bold={true}>Description</Text> (tells Claude when to use this agent):</Text></Box>;
|
|
||||||
$[54] = t12;
|
|
||||||
} else {
|
|
||||||
t12 = $[54];
|
|
||||||
}
|
|
||||||
if ($[55] !== whenToUsePreview) {
|
|
||||||
t13 = <Box marginLeft={2} marginTop={1}><Text>{whenToUsePreview}</Text></Box>;
|
|
||||||
$[55] = whenToUsePreview;
|
|
||||||
$[56] = t13;
|
|
||||||
} else {
|
|
||||||
t13 = $[56];
|
|
||||||
}
|
|
||||||
if ($[57] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t14 = <Box marginTop={1}><Text><Text bold={true}>System prompt</Text>:</Text></Box>;
|
|
||||||
$[57] = t14;
|
|
||||||
} else {
|
|
||||||
t14 = $[57];
|
|
||||||
}
|
|
||||||
if ($[58] !== systemPromptPreview) {
|
|
||||||
t15 = <Box marginLeft={2} marginTop={1}><Text>{systemPromptPreview}</Text></Box>;
|
|
||||||
$[58] = systemPromptPreview;
|
|
||||||
$[59] = t15;
|
|
||||||
} else {
|
|
||||||
t15 = $[59];
|
|
||||||
}
|
|
||||||
t16 = validation.warnings.length > 0 && <Box marginTop={1} flexDirection="column"><Text color="warning">Warnings:</Text>{validation.warnings.map(_temp2)}</Box>;
|
|
||||||
t17 = validation.errors.length > 0 && <Box marginTop={1} flexDirection="column"><Text color="error">Errors:</Text>{validation.errors.map(_temp3)}</Box>;
|
|
||||||
$[4] = agent;
|
|
||||||
$[5] = existingAgents;
|
|
||||||
$[6] = handleKeyDown;
|
|
||||||
$[7] = tools;
|
|
||||||
$[8] = wizardData.location;
|
|
||||||
$[9] = T0;
|
|
||||||
$[10] = T1;
|
|
||||||
$[11] = t10;
|
|
||||||
$[12] = t11;
|
|
||||||
$[13] = t12;
|
|
||||||
$[14] = t13;
|
|
||||||
$[15] = t14;
|
|
||||||
$[16] = t15;
|
|
||||||
$[17] = t16;
|
|
||||||
$[18] = t17;
|
|
||||||
$[19] = t18;
|
|
||||||
$[20] = t19;
|
|
||||||
$[21] = t3;
|
|
||||||
$[22] = t4;
|
|
||||||
$[23] = t5;
|
|
||||||
$[24] = t6;
|
|
||||||
$[25] = t7;
|
|
||||||
$[26] = t8;
|
|
||||||
$[27] = t9;
|
|
||||||
} else {
|
|
||||||
T0 = $[9];
|
|
||||||
T1 = $[10];
|
|
||||||
t10 = $[11];
|
|
||||||
t11 = $[12];
|
|
||||||
t12 = $[13];
|
|
||||||
t13 = $[14];
|
|
||||||
t14 = $[15];
|
|
||||||
t15 = $[16];
|
|
||||||
t16 = $[17];
|
|
||||||
t17 = $[18];
|
|
||||||
t18 = $[19];
|
|
||||||
t19 = $[20];
|
|
||||||
t3 = $[21];
|
|
||||||
t4 = $[22];
|
|
||||||
t5 = $[23];
|
|
||||||
t6 = $[24];
|
|
||||||
t7 = $[25];
|
|
||||||
t8 = $[26];
|
|
||||||
t9 = $[27];
|
|
||||||
}
|
|
||||||
let t20;
|
|
||||||
if ($[60] !== error) {
|
|
||||||
t20 = error && <Box marginTop={1}><Text color="error">{error}</Text></Box>;
|
|
||||||
$[60] = error;
|
|
||||||
$[61] = t20;
|
|
||||||
} else {
|
|
||||||
t20 = $[61];
|
|
||||||
}
|
|
||||||
let t21;
|
|
||||||
if ($[62] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t21 = <Text bold={true}>s</Text>;
|
|
||||||
$[62] = t21;
|
|
||||||
} else {
|
|
||||||
t21 = $[62];
|
|
||||||
}
|
|
||||||
let t22;
|
|
||||||
if ($[63] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t22 = <Text bold={true}>Enter</Text>;
|
|
||||||
$[63] = t22;
|
|
||||||
} else {
|
|
||||||
t22 = $[63];
|
|
||||||
}
|
|
||||||
let t23;
|
|
||||||
if ($[64] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t23 = <Box marginTop={2}><Text color="success">Press {t21} or {t22} to save,{" "}<Text bold={true}>e</Text> to save and edit</Text></Box>;
|
|
||||||
$[64] = t23;
|
|
||||||
} else {
|
|
||||||
t23 = $[64];
|
|
||||||
}
|
|
||||||
let t24;
|
|
||||||
if ($[65] !== T0 || $[66] !== t10 || $[67] !== t11 || $[68] !== t12 || $[69] !== t13 || $[70] !== t14 || $[71] !== t15 || $[72] !== t16 || $[73] !== t17 || $[74] !== t20 || $[75] !== t3 || $[76] !== t4 || $[77] !== t5 || $[78] !== t6 || $[79] !== t7 || $[80] !== t8 || $[81] !== t9) {
|
|
||||||
t24 = <T0 flexDirection={t3} tabIndex={t4} autoFocus={t5} onKeyDown={t6}>{t7}{t8}{t9}{t10}{t11}{t12}{t13}{t14}{t15}{t16}{t17}{t20}{t23}</T0>;
|
|
||||||
$[65] = T0;
|
|
||||||
$[66] = t10;
|
|
||||||
$[67] = t11;
|
|
||||||
$[68] = t12;
|
|
||||||
$[69] = t13;
|
|
||||||
$[70] = t14;
|
|
||||||
$[71] = t15;
|
|
||||||
$[72] = t16;
|
|
||||||
$[73] = t17;
|
|
||||||
$[74] = t20;
|
|
||||||
$[75] = t3;
|
|
||||||
$[76] = t4;
|
|
||||||
$[77] = t5;
|
|
||||||
$[78] = t6;
|
|
||||||
$[79] = t7;
|
|
||||||
$[80] = t8;
|
|
||||||
$[81] = t9;
|
|
||||||
$[82] = t24;
|
|
||||||
} else {
|
|
||||||
t24 = $[82];
|
|
||||||
}
|
|
||||||
let t25;
|
|
||||||
if ($[83] !== T1 || $[84] !== t18 || $[85] !== t19 || $[86] !== t24) {
|
|
||||||
t25 = <T1 subtitle={t18} footerText={t19}>{t24}</T1>;
|
|
||||||
$[83] = T1;
|
|
||||||
$[84] = t18;
|
|
||||||
$[85] = t19;
|
|
||||||
$[86] = t24;
|
|
||||||
$[87] = t25;
|
|
||||||
} else {
|
|
||||||
t25 = $[87];
|
|
||||||
}
|
|
||||||
return t25;
|
|
||||||
}
|
|
||||||
function _temp3(err, i_0) {
|
|
||||||
return <Text key={i_0} color="error">{" "}• {err}</Text>;
|
|
||||||
}
|
|
||||||
function _temp2(warning, i) {
|
|
||||||
return <Text key={i} dimColor={true}>{" "}• {warning}</Text>;
|
|
||||||
}
|
|
||||||
function _temp(toolNames) {
|
|
||||||
if (toolNames === undefined) {
|
|
||||||
return "All tools";
|
|
||||||
}
|
|
||||||
if (toolNames.length === 0) {
|
|
||||||
return "None";
|
|
||||||
}
|
|
||||||
if (toolNames.length === 1) {
|
|
||||||
return toolNames[0] || "None";
|
|
||||||
}
|
|
||||||
if (toolNames.length === 2) {
|
|
||||||
return toolNames.join(" and ");
|
|
||||||
}
|
|
||||||
return `${toolNames.slice(0, -1).join(", ")}, and ${toolNames[toolNames.length - 1]}`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,73 +1,112 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk'
|
||||||
import React, { type ReactNode, useCallback, useState } from 'react';
|
import React, { type ReactNode, useCallback, useState } from 'react'
|
||||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
import {
|
||||||
import { useSetAppState } from 'src/state/AppState.js';
|
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
import type { Tools } from '../../../../Tool.js';
|
logEvent,
|
||||||
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js';
|
} from 'src/services/analytics/index.js'
|
||||||
import { getActiveAgentsFromList } from '../../../../tools/AgentTool/loadAgentsDir.js';
|
import { useSetAppState } from 'src/state/AppState.js'
|
||||||
import { editFileInEditor } from '../../../../utils/promptEditor.js';
|
import type { Tools } from '../../../../Tool.js'
|
||||||
import { useWizard } from '../../../wizard/index.js';
|
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'
|
||||||
import { getNewAgentFilePath, saveAgentToFile } from '../../agentFileUtils.js';
|
import { getActiveAgentsFromList } from '../../../../tools/AgentTool/loadAgentsDir.js'
|
||||||
import type { AgentWizardData } from '../types.js';
|
import { editFileInEditor } from '../../../../utils/promptEditor.js'
|
||||||
import { ConfirmStep } from './ConfirmStep.js';
|
import { useWizard } from '../../../wizard/index.js'
|
||||||
|
import { getNewAgentFilePath, saveAgentToFile } from '../../agentFileUtils.js'
|
||||||
|
import type { AgentWizardData } from '../types.js'
|
||||||
|
import { ConfirmStep } from './ConfirmStep.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tools: Tools;
|
tools: Tools
|
||||||
existingAgents: AgentDefinition[];
|
existingAgents: AgentDefinition[]
|
||||||
onComplete: (message: string) => void;
|
onComplete: (message: string) => void
|
||||||
};
|
}
|
||||||
|
|
||||||
export function ConfirmStepWrapper({
|
export function ConfirmStepWrapper({
|
||||||
tools,
|
tools,
|
||||||
existingAgents,
|
existingAgents,
|
||||||
onComplete
|
onComplete,
|
||||||
}: Props): ReactNode {
|
}: Props): ReactNode {
|
||||||
const {
|
const { wizardData } = useWizard<AgentWizardData>()
|
||||||
wizardData
|
const [saveError, setSaveError] = useState<string | null>(null)
|
||||||
} = useWizard<AgentWizardData>();
|
const setAppState = useSetAppState()
|
||||||
const [saveError, setSaveError] = useState<string | null>(null);
|
|
||||||
const setAppState = useSetAppState();
|
const saveAgent = useCallback(
|
||||||
const saveAgent = useCallback(async (openInEditor: boolean): Promise<void> => {
|
async (openInEditor: boolean): Promise<void> => {
|
||||||
if (!wizardData?.finalAgent) return;
|
if (!wizardData?.finalAgent) return
|
||||||
try {
|
|
||||||
await saveAgentToFile(wizardData.location!, wizardData.finalAgent.agentType, wizardData.finalAgent.whenToUse, wizardData.finalAgent.tools, wizardData.finalAgent.getSystemPrompt(), true, wizardData.finalAgent.color, wizardData.finalAgent.model, wizardData.finalAgent.memory);
|
try {
|
||||||
setAppState(state => {
|
await saveAgentToFile(
|
||||||
if (!wizardData.finalAgent) return state;
|
wizardData.location!,
|
||||||
const allAgents = state.agentDefinitions.allAgents.concat(wizardData.finalAgent);
|
wizardData.finalAgent.agentType,
|
||||||
return {
|
wizardData.finalAgent.whenToUse,
|
||||||
...state,
|
wizardData.finalAgent.tools,
|
||||||
agentDefinitions: {
|
wizardData.finalAgent.getSystemPrompt(),
|
||||||
...state.agentDefinitions,
|
true,
|
||||||
activeAgents: getActiveAgentsFromList(allAgents),
|
wizardData.finalAgent.color,
|
||||||
allAgents
|
wizardData.finalAgent.model,
|
||||||
|
wizardData.finalAgent.memory,
|
||||||
|
)
|
||||||
|
|
||||||
|
setAppState(state => {
|
||||||
|
if (!wizardData.finalAgent) return state
|
||||||
|
|
||||||
|
const allAgents = state.agentDefinitions.allAgents.concat(
|
||||||
|
wizardData.finalAgent,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
agentDefinitions: {
|
||||||
|
...state.agentDefinitions,
|
||||||
|
activeAgents: getActiveAgentsFromList(allAgents),
|
||||||
|
allAgents,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
})
|
||||||
});
|
|
||||||
if (openInEditor) {
|
if (openInEditor) {
|
||||||
const filePath = getNewAgentFilePath({
|
const filePath = getNewAgentFilePath({
|
||||||
|
source: wizardData.location!,
|
||||||
|
agentType: wizardData.finalAgent.agentType,
|
||||||
|
})
|
||||||
|
await editFileInEditor(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
logEvent('tengu_agent_created', {
|
||||||
|
agent_type: wizardData.finalAgent.agentType,
|
||||||
|
generation_method: wizardData.wasGenerated ? 'generated' : 'manual',
|
||||||
source: wizardData.location!,
|
source: wizardData.location!,
|
||||||
agentType: wizardData.finalAgent.agentType
|
tool_count: wizardData.finalAgent.tools?.length ?? 'all',
|
||||||
});
|
has_custom_model: !!wizardData.finalAgent.model,
|
||||||
await editFileInEditor(filePath);
|
has_custom_color: !!wizardData.finalAgent.color,
|
||||||
|
has_memory: !!wizardData.finalAgent.memory,
|
||||||
|
memory_scope: wizardData.finalAgent.memory ?? 'none',
|
||||||
|
...(openInEditor ? { opened_in_editor: true } : {}),
|
||||||
|
} as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS)
|
||||||
|
|
||||||
|
const message = openInEditor
|
||||||
|
? `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)} and opened in editor. ` +
|
||||||
|
`If you made edits, restart to load the latest version.`
|
||||||
|
: `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)}`
|
||||||
|
onComplete(message)
|
||||||
|
} catch (err) {
|
||||||
|
setSaveError(
|
||||||
|
err instanceof Error ? err.message : 'Failed to save agent',
|
||||||
|
)
|
||||||
}
|
}
|
||||||
logEvent('tengu_agent_created', {
|
},
|
||||||
agent_type: wizardData.finalAgent.agentType,
|
[wizardData, onComplete, setAppState],
|
||||||
generation_method: wizardData.wasGenerated ? 'generated' : 'manual',
|
)
|
||||||
source: wizardData.location!,
|
|
||||||
tool_count: wizardData.finalAgent.tools?.length ?? 'all',
|
const handleSave = useCallback(() => saveAgent(false), [saveAgent])
|
||||||
has_custom_model: !!wizardData.finalAgent.model,
|
|
||||||
has_custom_color: !!wizardData.finalAgent.color,
|
const handleSaveAndEdit = useCallback(() => saveAgent(true), [saveAgent])
|
||||||
has_memory: !!wizardData.finalAgent.memory,
|
|
||||||
memory_scope: wizardData.finalAgent.memory ?? 'none',
|
return (
|
||||||
...(openInEditor ? {
|
<ConfirmStep
|
||||||
opened_in_editor: true
|
tools={tools}
|
||||||
} : {})
|
existingAgents={existingAgents}
|
||||||
} as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS);
|
onSave={handleSave}
|
||||||
const message = openInEditor ? `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)} and opened in editor. ` + `If you made edits, restart to load the latest version.` : `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)}`;
|
onSaveAndEdit={handleSaveAndEdit}
|
||||||
onComplete(message);
|
error={saveError}
|
||||||
} catch (err) {
|
/>
|
||||||
setSaveError(err instanceof Error ? err.message : 'Failed to save agent');
|
)
|
||||||
}
|
|
||||||
}, [wizardData, onComplete, setAppState]);
|
|
||||||
const handleSave = useCallback(() => saveAgent(false), [saveAgent]);
|
|
||||||
const handleSaveAndEdit = useCallback(() => saveAgent(true), [saveAgent]);
|
|
||||||
return <ConfirmStep tools={tools} existingAgents={existingAgents} onSave={handleSave} onSaveAndEdit={handleSaveAndEdit} error={saveError} />;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +1,94 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { type ReactNode, useCallback, useState } from 'react'
|
||||||
import React, { type ReactNode, useCallback, useState } from 'react';
|
import { Box, Text } from '../../../../ink.js'
|
||||||
import { Box, Text } from '../../../../ink.js';
|
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
|
||||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
|
import { editPromptInEditor } from '../../../../utils/promptEditor.js'
|
||||||
import { editPromptInEditor } from '../../../../utils/promptEditor.js';
|
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
import { Byline } from '../../../design-system/Byline.js'
|
||||||
import { Byline } from '../../../design-system/Byline.js';
|
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
import TextInput from '../../../TextInput.js'
|
||||||
import TextInput from '../../../TextInput.js';
|
import { useWizard } from '../../../wizard/index.js'
|
||||||
import { useWizard } from '../../../wizard/index.js';
|
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
import type { AgentWizardData } from '../types.js'
|
||||||
import type { AgentWizardData } from '../types.js';
|
|
||||||
export function DescriptionStep() {
|
export function DescriptionStep(): ReactNode {
|
||||||
const $ = _c(18);
|
const { goNext, goBack, updateWizardData, wizardData } =
|
||||||
const {
|
useWizard<AgentWizardData>()
|
||||||
goNext,
|
const [whenToUse, setWhenToUse] = useState(wizardData.whenToUse || '')
|
||||||
goBack,
|
const [cursorOffset, setCursorOffset] = useState(whenToUse.length)
|
||||||
updateWizardData,
|
const [error, setError] = useState<string | null>(null)
|
||||||
wizardData
|
|
||||||
} = useWizard();
|
// Handle escape key - use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
|
||||||
const [whenToUse, setWhenToUse] = useState(wizardData.whenToUse || "");
|
useKeybinding('confirm:no', goBack, { context: 'Settings' })
|
||||||
const [cursorOffset, setCursorOffset] = useState(whenToUse.length);
|
|
||||||
const [error, setError] = useState(null);
|
const handleExternalEditor = useCallback(async () => {
|
||||||
let t0;
|
const result = await editPromptInEditor(whenToUse)
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
if (result.content !== null) {
|
||||||
t0 = {
|
setWhenToUse(result.content)
|
||||||
context: "Settings"
|
setCursorOffset(result.content.length)
|
||||||
};
|
}
|
||||||
$[0] = t0;
|
}, [whenToUse])
|
||||||
} else {
|
|
||||||
t0 = $[0];
|
useKeybinding('chat:externalEditor', handleExternalEditor, {
|
||||||
|
context: 'Chat',
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = (value: string): void => {
|
||||||
|
const trimmedValue = value.trim()
|
||||||
|
if (!trimmedValue) {
|
||||||
|
setError('Description is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null)
|
||||||
|
updateWizardData({ whenToUse: trimmedValue })
|
||||||
|
goNext()
|
||||||
}
|
}
|
||||||
useKeybinding("confirm:no", goBack, t0);
|
|
||||||
let t1;
|
return (
|
||||||
if ($[1] !== whenToUse) {
|
<WizardDialogLayout
|
||||||
t1 = async () => {
|
subtitle="Description (tell Claude when to use this agent)"
|
||||||
const result = await editPromptInEditor(whenToUse);
|
footerText={
|
||||||
if (result.content !== null) {
|
<Byline>
|
||||||
setWhenToUse(result.content);
|
<KeyboardShortcutHint shortcut="Type" action="enter text" />
|
||||||
setCursorOffset(result.content.length);
|
<KeyboardShortcutHint shortcut="Enter" action="continue" />
|
||||||
|
<ConfigurableShortcutHint
|
||||||
|
action="chat:externalEditor"
|
||||||
|
context="Chat"
|
||||||
|
fallback="ctrl+g"
|
||||||
|
description="open in editor"
|
||||||
|
/>
|
||||||
|
<ConfigurableShortcutHint
|
||||||
|
action="confirm:no"
|
||||||
|
context="Settings"
|
||||||
|
fallback="Esc"
|
||||||
|
description="go back"
|
||||||
|
/>
|
||||||
|
</Byline>
|
||||||
}
|
}
|
||||||
};
|
>
|
||||||
$[1] = whenToUse;
|
<Box flexDirection="column">
|
||||||
$[2] = t1;
|
<Text>When should Claude use this agent?</Text>
|
||||||
} else {
|
|
||||||
t1 = $[2];
|
<Box marginTop={1}>
|
||||||
}
|
<TextInput
|
||||||
const handleExternalEditor = t1;
|
value={whenToUse}
|
||||||
let t2;
|
onChange={setWhenToUse}
|
||||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
onSubmit={handleSubmit}
|
||||||
t2 = {
|
placeholder="e.g., use this agent after you're done writing code..."
|
||||||
context: "Chat"
|
columns={80}
|
||||||
};
|
cursorOffset={cursorOffset}
|
||||||
$[3] = t2;
|
onChangeCursorOffset={setCursorOffset}
|
||||||
} else {
|
focus
|
||||||
t2 = $[3];
|
showCursor
|
||||||
}
|
/>
|
||||||
useKeybinding("chat:externalEditor", handleExternalEditor, t2);
|
</Box>
|
||||||
let t3;
|
|
||||||
if ($[4] !== goNext || $[5] !== updateWizardData) {
|
{error && (
|
||||||
t3 = value => {
|
<Box marginTop={1}>
|
||||||
const trimmedValue = value.trim();
|
<Text color="error">{error}</Text>
|
||||||
if (!trimmedValue) {
|
</Box>
|
||||||
setError("Description is required");
|
)}
|
||||||
return;
|
</Box>
|
||||||
}
|
</WizardDialogLayout>
|
||||||
setError(null);
|
)
|
||||||
updateWizardData({
|
|
||||||
whenToUse: trimmedValue
|
|
||||||
});
|
|
||||||
goNext();
|
|
||||||
};
|
|
||||||
$[4] = goNext;
|
|
||||||
$[5] = updateWizardData;
|
|
||||||
$[6] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[6];
|
|
||||||
}
|
|
||||||
const handleSubmit = t3;
|
|
||||||
let t4;
|
|
||||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t4 = <Byline><KeyboardShortcutHint shortcut="Type" action="enter text" /><KeyboardShortcutHint shortcut="Enter" action="continue" /><ConfigurableShortcutHint action="chat:externalEditor" context="Chat" fallback="ctrl+g" description="open in editor" /><ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="go back" /></Byline>;
|
|
||||||
$[7] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[7];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t5 = <Text>When should Claude use this agent?</Text>;
|
|
||||||
$[8] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[8];
|
|
||||||
}
|
|
||||||
let t6;
|
|
||||||
if ($[9] !== cursorOffset || $[10] !== handleSubmit || $[11] !== whenToUse) {
|
|
||||||
t6 = <Box marginTop={1}><TextInput value={whenToUse} onChange={setWhenToUse} onSubmit={handleSubmit} placeholder="e.g., use this agent after you're done writing code..." columns={80} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus={true} showCursor={true} /></Box>;
|
|
||||||
$[9] = cursorOffset;
|
|
||||||
$[10] = handleSubmit;
|
|
||||||
$[11] = whenToUse;
|
|
||||||
$[12] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[12];
|
|
||||||
}
|
|
||||||
let t7;
|
|
||||||
if ($[13] !== error) {
|
|
||||||
t7 = error && <Box marginTop={1}><Text color="error">{error}</Text></Box>;
|
|
||||||
$[13] = error;
|
|
||||||
$[14] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[14];
|
|
||||||
}
|
|
||||||
let t8;
|
|
||||||
if ($[15] !== t6 || $[16] !== t7) {
|
|
||||||
t8 = <WizardDialogLayout subtitle="Description (tell Claude when to use this agent)" footerText={t4}><Box flexDirection="column">{t5}{t6}{t7}</Box></WizardDialogLayout>;
|
|
||||||
$[15] = t6;
|
|
||||||
$[16] = t7;
|
|
||||||
$[17] = t8;
|
|
||||||
} else {
|
|
||||||
t8 = $[17];
|
|
||||||
}
|
|
||||||
return t8;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,57 @@
|
|||||||
import { APIUserAbortError } from '@anthropic-ai/sdk';
|
import { APIUserAbortError } from '@anthropic-ai/sdk'
|
||||||
import React, { type ReactNode, useCallback, useRef, useState } from 'react';
|
import React, { type ReactNode, useCallback, useRef, useState } from 'react'
|
||||||
import { useMainLoopModel } from '../../../../hooks/useMainLoopModel.js';
|
import { useMainLoopModel } from '../../../../hooks/useMainLoopModel.js'
|
||||||
import { Box, Text } from '../../../../ink.js';
|
import { Box, Text } from '../../../../ink.js'
|
||||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
|
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
|
||||||
import { createAbortController } from '../../../../utils/abortController.js';
|
import { createAbortController } from '../../../../utils/abortController.js'
|
||||||
import { editPromptInEditor } from '../../../../utils/promptEditor.js';
|
import { editPromptInEditor } from '../../../../utils/promptEditor.js'
|
||||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||||
import { Byline } from '../../../design-system/Byline.js';
|
import { Byline } from '../../../design-system/Byline.js'
|
||||||
import { Spinner } from '../../../Spinner.js';
|
import { Spinner } from '../../../Spinner.js'
|
||||||
import TextInput from '../../../TextInput.js';
|
import TextInput from '../../../TextInput.js'
|
||||||
import { useWizard } from '../../../wizard/index.js';
|
import { useWizard } from '../../../wizard/index.js'
|
||||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||||
import { generateAgent } from '../../generateAgent.js';
|
import { generateAgent } from '../../generateAgent.js'
|
||||||
import type { AgentWizardData } from '../types.js';
|
import type { AgentWizardData } from '../types.js'
|
||||||
|
|
||||||
export function GenerateStep(): ReactNode {
|
export function GenerateStep(): ReactNode {
|
||||||
const {
|
const { updateWizardData, goBack, goToStep, wizardData } =
|
||||||
updateWizardData,
|
useWizard<AgentWizardData>()
|
||||||
goBack,
|
const [prompt, setPrompt] = useState(wizardData.generationPrompt || '')
|
||||||
goToStep,
|
const [isGenerating, setIsGenerating] = useState(false)
|
||||||
wizardData
|
const [error, setError] = useState<string | null>(null)
|
||||||
} = useWizard<AgentWizardData>();
|
const [cursorOffset, setCursorOffset] = useState(prompt.length)
|
||||||
const [prompt, setPrompt] = useState(wizardData.generationPrompt || '');
|
const model = useMainLoopModel()
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
const abortControllerRef = useRef<AbortController | null>(null)
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [cursorOffset, setCursorOffset] = useState(prompt.length);
|
|
||||||
const model = useMainLoopModel();
|
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
|
||||||
|
|
||||||
// Cancel generation when escape pressed during generation
|
// Cancel generation when escape pressed during generation
|
||||||
const handleCancelGeneration = useCallback(() => {
|
const handleCancelGeneration = useCallback(() => {
|
||||||
if (abortControllerRef.current) {
|
if (abortControllerRef.current) {
|
||||||
abortControllerRef.current.abort();
|
abortControllerRef.current.abort()
|
||||||
abortControllerRef.current = null;
|
abortControllerRef.current = null
|
||||||
setIsGenerating(false);
|
setIsGenerating(false)
|
||||||
setError('Generation cancelled');
|
setError('Generation cancelled')
|
||||||
}
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in prompt input)
|
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in prompt input)
|
||||||
useKeybinding('confirm:no', handleCancelGeneration, {
|
useKeybinding('confirm:no', handleCancelGeneration, {
|
||||||
context: 'Settings',
|
context: 'Settings',
|
||||||
isActive: isGenerating
|
isActive: isGenerating,
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleExternalEditor = useCallback(async () => {
|
const handleExternalEditor = useCallback(async () => {
|
||||||
const result = await editPromptInEditor(prompt);
|
const result = await editPromptInEditor(prompt)
|
||||||
if (result.content !== null) {
|
if (result.content !== null) {
|
||||||
setPrompt(result.content);
|
setPrompt(result.content)
|
||||||
setCursorOffset(result.content.length);
|
setCursorOffset(result.content.length)
|
||||||
}
|
}
|
||||||
}, [prompt]);
|
}, [prompt])
|
||||||
|
|
||||||
useKeybinding('chat:externalEditor', handleExternalEditor, {
|
useKeybinding('chat:externalEditor', handleExternalEditor, {
|
||||||
context: 'Chat',
|
context: 'Chat',
|
||||||
isActive: !isGenerating
|
isActive: !isGenerating,
|
||||||
});
|
})
|
||||||
|
|
||||||
// Go back when escape pressed while not generating
|
// Go back when escape pressed while not generating
|
||||||
const handleGoBack = useCallback(() => {
|
const handleGoBack = useCallback(() => {
|
||||||
@@ -62,81 +61,141 @@ export function GenerateStep(): ReactNode {
|
|||||||
systemPrompt: '',
|
systemPrompt: '',
|
||||||
whenToUse: '',
|
whenToUse: '',
|
||||||
generatedAgent: undefined,
|
generatedAgent: undefined,
|
||||||
wasGenerated: false
|
wasGenerated: false,
|
||||||
});
|
})
|
||||||
setPrompt('');
|
setPrompt('')
|
||||||
setError(null);
|
setError(null)
|
||||||
goBack();
|
goBack()
|
||||||
}, [updateWizardData, goBack]);
|
}, [updateWizardData, goBack])
|
||||||
|
|
||||||
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in prompt input)
|
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in prompt input)
|
||||||
useKeybinding('confirm:no', handleGoBack, {
|
useKeybinding('confirm:no', handleGoBack, {
|
||||||
context: 'Settings',
|
context: 'Settings',
|
||||||
isActive: !isGenerating
|
isActive: !isGenerating,
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleGenerate = async (): Promise<void> => {
|
const handleGenerate = async (): Promise<void> => {
|
||||||
const trimmedPrompt = prompt.trim();
|
const trimmedPrompt = prompt.trim()
|
||||||
if (!trimmedPrompt) {
|
if (!trimmedPrompt) {
|
||||||
setError('Please describe what the agent should do');
|
setError('Please describe what the agent should do')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
setError(null);
|
|
||||||
setIsGenerating(true);
|
setError(null)
|
||||||
|
setIsGenerating(true)
|
||||||
updateWizardData({
|
updateWizardData({
|
||||||
generationPrompt: trimmedPrompt,
|
generationPrompt: trimmedPrompt,
|
||||||
isGenerating: true
|
isGenerating: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
// Create abort controller for this generation
|
// Create abort controller for this generation
|
||||||
const controller = createAbortController();
|
const controller = createAbortController()
|
||||||
abortControllerRef.current = controller;
|
abortControllerRef.current = controller
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const generated = await generateAgent(trimmedPrompt, model, [], controller.signal);
|
const generated = await generateAgent(
|
||||||
|
trimmedPrompt,
|
||||||
|
model,
|
||||||
|
[],
|
||||||
|
controller.signal,
|
||||||
|
)
|
||||||
|
|
||||||
updateWizardData({
|
updateWizardData({
|
||||||
agentType: generated.identifier,
|
agentType: generated.identifier,
|
||||||
whenToUse: generated.whenToUse,
|
whenToUse: generated.whenToUse,
|
||||||
systemPrompt: generated.systemPrompt,
|
systemPrompt: generated.systemPrompt,
|
||||||
generatedAgent: generated,
|
generatedAgent: generated,
|
||||||
isGenerating: false,
|
isGenerating: false,
|
||||||
wasGenerated: true
|
wasGenerated: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
// Skip directly to ToolsStep (index 6) - matching original flow
|
// Skip directly to ToolsStep (index 6) - matching original flow
|
||||||
goToStep(6);
|
goToStep(6)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Don't show error if it was cancelled (already set in escape handler)
|
// Don't show error if it was cancelled (already set in escape handler)
|
||||||
if (err instanceof APIUserAbortError) {
|
if (err instanceof APIUserAbortError) {
|
||||||
// User cancelled - no error to show
|
// User cancelled - no error to show
|
||||||
} else if (err instanceof Error && !err.message.includes('No assistant message found')) {
|
} else if (
|
||||||
setError(err.message || 'Failed to generate agent');
|
err instanceof Error &&
|
||||||
|
!err.message.includes('No assistant message found')
|
||||||
|
) {
|
||||||
|
setError(err.message || 'Failed to generate agent')
|
||||||
}
|
}
|
||||||
updateWizardData({
|
updateWizardData({ isGenerating: false })
|
||||||
isGenerating: false
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsGenerating(false);
|
setIsGenerating(false)
|
||||||
abortControllerRef.current = null;
|
abortControllerRef.current = null
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
const subtitle = 'Describe what this agent should do and when it should be used (be comprehensive for best results)';
|
|
||||||
|
const subtitle =
|
||||||
|
'Describe what this agent should do and when it should be used (be comprehensive for best results)'
|
||||||
|
|
||||||
if (isGenerating) {
|
if (isGenerating) {
|
||||||
return <WizardDialogLayout subtitle={subtitle} footerText={<ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" />}>
|
return (
|
||||||
|
<WizardDialogLayout
|
||||||
|
subtitle={subtitle}
|
||||||
|
footerText={
|
||||||
|
<ConfigurableShortcutHint
|
||||||
|
action="confirm:no"
|
||||||
|
context="Settings"
|
||||||
|
fallback="Esc"
|
||||||
|
description="cancel"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Box flexDirection="row" alignItems="center">
|
<Box flexDirection="row" alignItems="center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
<Text color="suggestion"> Generating agent from description...</Text>
|
<Text color="suggestion"> Generating agent from description...</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</WizardDialogLayout>;
|
</WizardDialogLayout>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return <WizardDialogLayout subtitle={subtitle} footerText={<Byline>
|
|
||||||
<ConfigurableShortcutHint action="confirm:yes" context="Confirmation" fallback="Enter" description="submit" />
|
return (
|
||||||
<ConfigurableShortcutHint action="chat:externalEditor" context="Chat" fallback="ctrl+g" description="open in editor" />
|
<WizardDialogLayout
|
||||||
<ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="go back" />
|
subtitle={subtitle}
|
||||||
</Byline>}>
|
footerText={
|
||||||
|
<Byline>
|
||||||
|
<ConfigurableShortcutHint
|
||||||
|
action="confirm:yes"
|
||||||
|
context="Confirmation"
|
||||||
|
fallback="Enter"
|
||||||
|
description="submit"
|
||||||
|
/>
|
||||||
|
<ConfigurableShortcutHint
|
||||||
|
action="chat:externalEditor"
|
||||||
|
context="Chat"
|
||||||
|
fallback="ctrl+g"
|
||||||
|
description="open in editor"
|
||||||
|
/>
|
||||||
|
<ConfigurableShortcutHint
|
||||||
|
action="confirm:no"
|
||||||
|
context="Settings"
|
||||||
|
fallback="Esc"
|
||||||
|
description="go back"
|
||||||
|
/>
|
||||||
|
</Byline>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{error && <Box marginBottom={1}>
|
{error && (
|
||||||
|
<Box marginBottom={1}>
|
||||||
<Text color="error">{error}</Text>
|
<Text color="error">{error}</Text>
|
||||||
</Box>}
|
</Box>
|
||||||
<TextInput value={prompt} onChange={setPrompt} onSubmit={handleGenerate} placeholder="e.g., Help me write unit tests for my code..." columns={80} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus showCursor />
|
)}
|
||||||
|
<TextInput
|
||||||
|
value={prompt}
|
||||||
|
onChange={setPrompt}
|
||||||
|
onSubmit={handleGenerate}
|
||||||
|
placeholder="e.g., Help me write unit tests for my code..."
|
||||||
|
columns={80}
|
||||||
|
cursorOffset={cursorOffset}
|
||||||
|
onChangeCursorOffset={setCursorOffset}
|
||||||
|
focus
|
||||||
|
showCursor
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</WizardDialogLayout>;
|
</WizardDialogLayout>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +1,55 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { type ReactNode } from 'react'
|
||||||
import React, { type ReactNode } from 'react';
|
import { Box } from '../../../../ink.js'
|
||||||
import { Box } from '../../../../ink.js';
|
import type { SettingSource } from '../../../../utils/settings/constants.js'
|
||||||
import type { SettingSource } from '../../../../utils/settings/constants.js';
|
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
import { Select } from '../../../CustomSelect/select.js'
|
||||||
import { Select } from '../../../CustomSelect/select.js';
|
import { Byline } from '../../../design-system/Byline.js'
|
||||||
import { Byline } from '../../../design-system/Byline.js';
|
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
import { useWizard } from '../../../wizard/index.js'
|
||||||
import { useWizard } from '../../../wizard/index.js';
|
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
import type { AgentWizardData } from '../types.js'
|
||||||
import type { AgentWizardData } from '../types.js';
|
|
||||||
export function LocationStep() {
|
export function LocationStep(): ReactNode {
|
||||||
const $ = _c(11);
|
const { goNext, updateWizardData, cancel } = useWizard<AgentWizardData>()
|
||||||
const {
|
|
||||||
goNext,
|
const locationOptions = [
|
||||||
updateWizardData,
|
{
|
||||||
cancel
|
label: 'Project (.claude/agents/)',
|
||||||
} = useWizard();
|
value: 'projectSettings' as SettingSource,
|
||||||
let t0;
|
},
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
{
|
||||||
t0 = {
|
label: 'Personal (~/.claude/agents/)',
|
||||||
label: "Project (.claude/agents/)",
|
value: 'userSettings' as SettingSource,
|
||||||
value: "projectSettings" as SettingSource
|
},
|
||||||
};
|
]
|
||||||
$[0] = t0;
|
|
||||||
} else {
|
return (
|
||||||
t0 = $[0];
|
<WizardDialogLayout
|
||||||
}
|
subtitle="Choose location"
|
||||||
let t1;
|
footerText={
|
||||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
<Byline>
|
||||||
t1 = [t0, {
|
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||||
label: "Personal (~/.claude/agents/)",
|
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
||||||
value: "userSettings" as SettingSource
|
<ConfigurableShortcutHint
|
||||||
}];
|
action="confirm:no"
|
||||||
$[1] = t1;
|
context="Confirmation"
|
||||||
} else {
|
fallback="Esc"
|
||||||
t1 = $[1];
|
description="cancel"
|
||||||
}
|
/>
|
||||||
const locationOptions = t1;
|
</Byline>
|
||||||
let t2;
|
}
|
||||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
>
|
||||||
t2 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /></Byline>;
|
<Box>
|
||||||
$[2] = t2;
|
<Select
|
||||||
} else {
|
key="location-select"
|
||||||
t2 = $[2];
|
options={locationOptions}
|
||||||
}
|
onChange={(value: string) => {
|
||||||
let t3;
|
updateWizardData({ location: value as SettingSource })
|
||||||
if ($[3] !== goNext || $[4] !== updateWizardData) {
|
goNext()
|
||||||
t3 = value => {
|
}}
|
||||||
updateWizardData({
|
onCancel={() => cancel()}
|
||||||
location: value as SettingSource
|
/>
|
||||||
});
|
</Box>
|
||||||
goNext();
|
</WizardDialogLayout>
|
||||||
};
|
)
|
||||||
$[3] = goNext;
|
|
||||||
$[4] = updateWizardData;
|
|
||||||
$[5] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[5];
|
|
||||||
}
|
|
||||||
let t4;
|
|
||||||
if ($[6] !== cancel) {
|
|
||||||
t4 = () => cancel();
|
|
||||||
$[6] = cancel;
|
|
||||||
$[7] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[7];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[8] !== t3 || $[9] !== t4) {
|
|
||||||
t5 = <WizardDialogLayout subtitle="Choose location" footerText={t2}><Box><Select key="location-select" options={locationOptions} onChange={t3} onCancel={t4} /></Box></WizardDialogLayout>;
|
|
||||||
$[8] = t3;
|
|
||||||
$[9] = t4;
|
|
||||||
$[10] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[10];
|
|
||||||
}
|
|
||||||
return t5;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,112 +1,102 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { type ReactNode } from 'react'
|
||||||
import React, { type ReactNode } from 'react';
|
import { Box } from '../../../../ink.js'
|
||||||
import { Box } from '../../../../ink.js';
|
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
|
||||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
|
import { isAutoMemoryEnabled } from '../../../../memdir/paths.js'
|
||||||
import { isAutoMemoryEnabled } from '../../../../memdir/paths.js';
|
import {
|
||||||
import { type AgentMemoryScope, loadAgentMemoryPrompt } from '../../../../tools/AgentTool/agentMemory.js';
|
type AgentMemoryScope,
|
||||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
loadAgentMemoryPrompt,
|
||||||
import { Select } from '../../../CustomSelect/select.js';
|
} from '../../../../tools/AgentTool/agentMemory.js'
|
||||||
import { Byline } from '../../../design-system/Byline.js';
|
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
import { Select } from '../../../CustomSelect/select.js'
|
||||||
import { useWizard } from '../../../wizard/index.js';
|
import { Byline } from '../../../design-system/Byline.js'
|
||||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||||
import type { AgentWizardData } from '../types.js';
|
import { useWizard } from '../../../wizard/index.js'
|
||||||
|
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||||
|
import type { AgentWizardData } from '../types.js'
|
||||||
|
|
||||||
type MemoryOption = {
|
type MemoryOption = {
|
||||||
label: string;
|
label: string
|
||||||
value: AgentMemoryScope | 'none';
|
value: AgentMemoryScope | 'none'
|
||||||
};
|
}
|
||||||
export function MemoryStep() {
|
|
||||||
const $ = _c(13);
|
export function MemoryStep(): ReactNode {
|
||||||
const {
|
const { goNext, goBack, updateWizardData, wizardData } =
|
||||||
goNext,
|
useWizard<AgentWizardData>()
|
||||||
goBack,
|
|
||||||
updateWizardData,
|
useKeybinding('confirm:no', goBack, { context: 'Confirmation' })
|
||||||
wizardData
|
|
||||||
} = useWizard();
|
const isUserScope = wizardData.location === 'userSettings'
|
||||||
let t0;
|
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
// Build options with the recommended default first, then alternatives
|
||||||
t0 = {
|
// The recommended scope matches the agent's location (project agent → project memory, user agent → user memory)
|
||||||
context: "Confirmation"
|
const memoryOptions: MemoryOption[] = isUserScope
|
||||||
};
|
? [
|
||||||
$[0] = t0;
|
{
|
||||||
} else {
|
label: 'User scope (~/.claude/agent-memory/) (Recommended)',
|
||||||
t0 = $[0];
|
value: 'user',
|
||||||
}
|
},
|
||||||
useKeybinding("confirm:no", goBack, t0);
|
{ label: 'None (no persistent memory)', value: 'none' },
|
||||||
const isUserScope = wizardData.location === "userSettings";
|
{ label: 'Project scope (.claude/agent-memory/)', value: 'project' },
|
||||||
let t1;
|
{ label: 'Local scope (.claude/agent-memory-local/)', value: 'local' },
|
||||||
if ($[1] !== isUserScope) {
|
]
|
||||||
t1 = isUserScope ? [{
|
: [
|
||||||
label: "User scope (~/.claude/agent-memory/) (Recommended)",
|
{
|
||||||
value: "user"
|
label: 'Project scope (.claude/agent-memory/) (Recommended)',
|
||||||
}, {
|
value: 'project',
|
||||||
label: "None (no persistent memory)",
|
},
|
||||||
value: "none"
|
{ label: 'None (no persistent memory)', value: 'none' },
|
||||||
}, {
|
{ label: 'User scope (~/.claude/agent-memory/)', value: 'user' },
|
||||||
label: "Project scope (.claude/agent-memory/)",
|
{ label: 'Local scope (.claude/agent-memory-local/)', value: 'local' },
|
||||||
value: "project"
|
]
|
||||||
}, {
|
|
||||||
label: "Local scope (.claude/agent-memory-local/)",
|
const handleSelect = (value: string): void => {
|
||||||
value: "local"
|
const memory = value === 'none' ? undefined : (value as AgentMemoryScope)
|
||||||
}] : [{
|
const agentType = wizardData.finalAgent?.agentType
|
||||||
label: "Project scope (.claude/agent-memory/) (Recommended)",
|
updateWizardData({
|
||||||
value: "project"
|
selectedMemory: memory,
|
||||||
}, {
|
// Update finalAgent with memory and rewire getSystemPrompt to include memory loading.
|
||||||
label: "None (no persistent memory)",
|
// Explicitly set memory (not conditional spread) so selecting 'none' after going back clears it.
|
||||||
value: "none"
|
finalAgent: wizardData.finalAgent
|
||||||
}, {
|
? {
|
||||||
label: "User scope (~/.claude/agent-memory/)",
|
...wizardData.finalAgent,
|
||||||
value: "user"
|
memory,
|
||||||
}, {
|
getSystemPrompt:
|
||||||
label: "Local scope (.claude/agent-memory-local/)",
|
isAutoMemoryEnabled() && memory && agentType
|
||||||
value: "local"
|
? () =>
|
||||||
}];
|
wizardData.systemPrompt! +
|
||||||
$[1] = isUserScope;
|
'\n\n' +
|
||||||
$[2] = t1;
|
loadAgentMemoryPrompt(agentType, memory)
|
||||||
} else {
|
: () => wizardData.systemPrompt!,
|
||||||
t1 = $[2];
|
}
|
||||||
}
|
: undefined,
|
||||||
const memoryOptions = t1;
|
})
|
||||||
let t2;
|
goNext()
|
||||||
if ($[3] !== goNext || $[4] !== updateWizardData || $[5] !== wizardData.finalAgent || $[6] !== wizardData.systemPrompt) {
|
}
|
||||||
t2 = value => {
|
|
||||||
const memory = value === "none" ? undefined : value as AgentMemoryScope;
|
return (
|
||||||
const agentType = wizardData.finalAgent?.agentType;
|
<WizardDialogLayout
|
||||||
updateWizardData({
|
subtitle="Configure agent memory"
|
||||||
selectedMemory: memory,
|
footerText={
|
||||||
finalAgent: wizardData.finalAgent ? {
|
<Byline>
|
||||||
...wizardData.finalAgent,
|
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||||
memory,
|
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
||||||
getSystemPrompt: isAutoMemoryEnabled() && memory && agentType ? () => wizardData.systemPrompt + "\n\n" + loadAgentMemoryPrompt(agentType, memory) : () => wizardData.systemPrompt
|
<ConfigurableShortcutHint
|
||||||
} : undefined
|
action="confirm:no"
|
||||||
});
|
context="Confirmation"
|
||||||
goNext();
|
fallback="Esc"
|
||||||
};
|
description="go back"
|
||||||
$[3] = goNext;
|
/>
|
||||||
$[4] = updateWizardData;
|
</Byline>
|
||||||
$[5] = wizardData.finalAgent;
|
}
|
||||||
$[6] = wizardData.systemPrompt;
|
>
|
||||||
$[7] = t2;
|
<Box>
|
||||||
} else {
|
<Select
|
||||||
t2 = $[7];
|
key="memory-select"
|
||||||
}
|
options={memoryOptions}
|
||||||
const handleSelect = t2;
|
onChange={handleSelect}
|
||||||
let t3;
|
onCancel={goBack}
|
||||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
/>
|
||||||
t3 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
|
</Box>
|
||||||
$[8] = t3;
|
</WizardDialogLayout>
|
||||||
} else {
|
)
|
||||||
t3 = $[8];
|
|
||||||
}
|
|
||||||
let t4;
|
|
||||||
if ($[9] !== goBack || $[10] !== handleSelect || $[11] !== memoryOptions) {
|
|
||||||
t4 = <WizardDialogLayout subtitle="Configure agent memory" footerText={t3}><Box><Select key="memory-select" options={memoryOptions} onChange={handleSelect} onCancel={goBack} /></Box></WizardDialogLayout>;
|
|
||||||
$[9] = goBack;
|
|
||||||
$[10] = handleSelect;
|
|
||||||
$[11] = memoryOptions;
|
|
||||||
$[12] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[12];
|
|
||||||
}
|
|
||||||
return t4;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +1,65 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { type ReactNode } from 'react'
|
||||||
import React, { type ReactNode } from 'react';
|
import { Box } from '../../../../ink.js'
|
||||||
import { Box } from '../../../../ink.js';
|
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
import { Select } from '../../../CustomSelect/select.js'
|
||||||
import { Select } from '../../../CustomSelect/select.js';
|
import { Byline } from '../../../design-system/Byline.js'
|
||||||
import { Byline } from '../../../design-system/Byline.js';
|
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
import { useWizard } from '../../../wizard/index.js'
|
||||||
import { useWizard } from '../../../wizard/index.js';
|
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
import type { AgentWizardData } from '../types.js'
|
||||||
import type { AgentWizardData } from '../types.js';
|
|
||||||
export function MethodStep() {
|
export function MethodStep(): ReactNode {
|
||||||
const $ = _c(11);
|
const { goNext, goBack, updateWizardData, goToStep } =
|
||||||
const {
|
useWizard<AgentWizardData>()
|
||||||
goNext,
|
|
||||||
goBack,
|
const methodOptions = [
|
||||||
updateWizardData,
|
{
|
||||||
goToStep
|
label: 'Generate with Claude (recommended)',
|
||||||
} = useWizard();
|
value: 'generate',
|
||||||
let t0;
|
},
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
{
|
||||||
t0 = [{
|
label: 'Manual configuration',
|
||||||
label: "Generate with Claude (recommended)",
|
value: 'manual',
|
||||||
value: "generate"
|
},
|
||||||
}, {
|
]
|
||||||
label: "Manual configuration",
|
|
||||||
value: "manual"
|
return (
|
||||||
}];
|
<WizardDialogLayout
|
||||||
$[0] = t0;
|
subtitle="Creation method"
|
||||||
} else {
|
footerText={
|
||||||
t0 = $[0];
|
<Byline>
|
||||||
}
|
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||||
const methodOptions = t0;
|
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
||||||
let t1;
|
<ConfigurableShortcutHint
|
||||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
action="confirm:no"
|
||||||
t1 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
|
context="Confirmation"
|
||||||
$[1] = t1;
|
fallback="Esc"
|
||||||
} else {
|
description="go back"
|
||||||
t1 = $[1];
|
/>
|
||||||
}
|
</Byline>
|
||||||
let t2;
|
|
||||||
if ($[2] !== goNext || $[3] !== goToStep || $[4] !== updateWizardData) {
|
|
||||||
t2 = value => {
|
|
||||||
const method = value as 'generate' | 'manual';
|
|
||||||
updateWizardData({
|
|
||||||
method,
|
|
||||||
wasGenerated: method === "generate"
|
|
||||||
});
|
|
||||||
if (method === "generate") {
|
|
||||||
goNext();
|
|
||||||
} else {
|
|
||||||
goToStep(3);
|
|
||||||
}
|
}
|
||||||
};
|
>
|
||||||
$[2] = goNext;
|
<Box>
|
||||||
$[3] = goToStep;
|
<Select
|
||||||
$[4] = updateWizardData;
|
key="method-select"
|
||||||
$[5] = t2;
|
options={methodOptions}
|
||||||
} else {
|
onChange={(value: string) => {
|
||||||
t2 = $[5];
|
const method = value as 'generate' | 'manual'
|
||||||
}
|
updateWizardData({
|
||||||
let t3;
|
method,
|
||||||
if ($[6] !== goBack) {
|
wasGenerated: method === 'generate',
|
||||||
t3 = () => goBack();
|
})
|
||||||
$[6] = goBack;
|
|
||||||
$[7] = t3;
|
// Dynamic navigation based on method
|
||||||
} else {
|
if (method === 'generate') {
|
||||||
t3 = $[7];
|
goNext() // Go to GenerateStep (index 2)
|
||||||
}
|
} else {
|
||||||
let t4;
|
goToStep(3) // Skip to TypeStep (index 3)
|
||||||
if ($[8] !== t2 || $[9] !== t3) {
|
}
|
||||||
t4 = <WizardDialogLayout subtitle="Creation method" footerText={t1}><Box><Select key="method-select" options={methodOptions} onChange={t2} onCancel={t3} /></Box></WizardDialogLayout>;
|
}}
|
||||||
$[8] = t2;
|
onCancel={() => goBack()}
|
||||||
$[9] = t3;
|
/>
|
||||||
$[10] = t4;
|
</Box>
|
||||||
} else {
|
</WizardDialogLayout>
|
||||||
t4 = $[10];
|
)
|
||||||
}
|
|
||||||
return t4;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,42 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { type ReactNode } from 'react'
|
||||||
import React, { type ReactNode } from 'react';
|
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
import { Byline } from '../../../design-system/Byline.js'
|
||||||
import { Byline } from '../../../design-system/Byline.js';
|
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
import { useWizard } from '../../../wizard/index.js'
|
||||||
import { useWizard } from '../../../wizard/index.js';
|
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
import { ModelSelector } from '../../ModelSelector.js'
|
||||||
import { ModelSelector } from '../../ModelSelector.js';
|
import type { AgentWizardData } from '../types.js'
|
||||||
import type { AgentWizardData } from '../types.js';
|
|
||||||
export function ModelStep() {
|
export function ModelStep(): ReactNode {
|
||||||
const $ = _c(8);
|
const { goNext, goBack, updateWizardData, wizardData } =
|
||||||
const {
|
useWizard<AgentWizardData>()
|
||||||
goNext,
|
|
||||||
goBack,
|
const handleComplete = (model?: string): void => {
|
||||||
updateWizardData,
|
updateWizardData({ selectedModel: model })
|
||||||
wizardData
|
goNext()
|
||||||
} = useWizard();
|
|
||||||
let t0;
|
|
||||||
if ($[0] !== goNext || $[1] !== updateWizardData) {
|
|
||||||
t0 = model => {
|
|
||||||
updateWizardData({
|
|
||||||
selectedModel: model
|
|
||||||
});
|
|
||||||
goNext();
|
|
||||||
};
|
|
||||||
$[0] = goNext;
|
|
||||||
$[1] = updateWizardData;
|
|
||||||
$[2] = t0;
|
|
||||||
} else {
|
|
||||||
t0 = $[2];
|
|
||||||
}
|
}
|
||||||
const handleComplete = t0;
|
|
||||||
let t1;
|
return (
|
||||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
<WizardDialogLayout
|
||||||
t1 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
|
subtitle="Select model"
|
||||||
$[3] = t1;
|
footerText={
|
||||||
} else {
|
<Byline>
|
||||||
t1 = $[3];
|
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||||
}
|
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
||||||
let t2;
|
<ConfigurableShortcutHint
|
||||||
if ($[4] !== goBack || $[5] !== handleComplete || $[6] !== wizardData.selectedModel) {
|
action="confirm:no"
|
||||||
t2 = <WizardDialogLayout subtitle="Select model" footerText={t1}><ModelSelector initialModel={wizardData.selectedModel} onComplete={handleComplete} onCancel={goBack} /></WizardDialogLayout>;
|
context="Confirmation"
|
||||||
$[4] = goBack;
|
fallback="Esc"
|
||||||
$[5] = handleComplete;
|
description="go back"
|
||||||
$[6] = wizardData.selectedModel;
|
/>
|
||||||
$[7] = t2;
|
</Byline>
|
||||||
} else {
|
}
|
||||||
t2 = $[7];
|
>
|
||||||
}
|
<ModelSelector
|
||||||
return t2;
|
initialModel={wizardData.selectedModel}
|
||||||
|
onComplete={handleComplete}
|
||||||
|
onCancel={goBack}
|
||||||
|
/>
|
||||||
|
</WizardDialogLayout>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,127 +1,97 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { type ReactNode, useCallback, useState } from 'react'
|
||||||
import React, { type ReactNode, useCallback, useState } from 'react';
|
import { Box, Text } from '../../../../ink.js'
|
||||||
import { Box, Text } from '../../../../ink.js';
|
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
|
||||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
|
import { editPromptInEditor } from '../../../../utils/promptEditor.js'
|
||||||
import { editPromptInEditor } from '../../../../utils/promptEditor.js';
|
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
import { Byline } from '../../../design-system/Byline.js'
|
||||||
import { Byline } from '../../../design-system/Byline.js';
|
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
import TextInput from '../../../TextInput.js'
|
||||||
import TextInput from '../../../TextInput.js';
|
import { useWizard } from '../../../wizard/index.js'
|
||||||
import { useWizard } from '../../../wizard/index.js';
|
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
import type { AgentWizardData } from '../types.js'
|
||||||
import type { AgentWizardData } from '../types.js';
|
|
||||||
export function PromptStep() {
|
export function PromptStep(): ReactNode {
|
||||||
const $ = _c(20);
|
const { goNext, goBack, updateWizardData, wizardData } =
|
||||||
const {
|
useWizard<AgentWizardData>()
|
||||||
goNext,
|
const [systemPrompt, setSystemPrompt] = useState(
|
||||||
goBack,
|
wizardData.systemPrompt || '',
|
||||||
updateWizardData,
|
)
|
||||||
wizardData
|
const [cursorOffset, setCursorOffset] = useState(systemPrompt.length)
|
||||||
} = useWizard();
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [systemPrompt, setSystemPrompt] = useState(wizardData.systemPrompt || "");
|
|
||||||
const [cursorOffset, setCursorOffset] = useState(systemPrompt.length);
|
// Handle escape key - use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
|
||||||
const [error, setError] = useState(null);
|
useKeybinding('confirm:no', goBack, { context: 'Settings' })
|
||||||
let t0;
|
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
const handleExternalEditor = useCallback(async () => {
|
||||||
t0 = {
|
const result = await editPromptInEditor(systemPrompt)
|
||||||
context: "Settings"
|
if (result.content !== null) {
|
||||||
};
|
setSystemPrompt(result.content)
|
||||||
$[0] = t0;
|
setCursorOffset(result.content.length)
|
||||||
} else {
|
}
|
||||||
t0 = $[0];
|
}, [systemPrompt])
|
||||||
|
|
||||||
|
useKeybinding('chat:externalEditor', handleExternalEditor, {
|
||||||
|
context: 'Chat',
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = (): void => {
|
||||||
|
const trimmedPrompt = systemPrompt.trim()
|
||||||
|
if (!trimmedPrompt) {
|
||||||
|
setError('System prompt is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null)
|
||||||
|
updateWizardData({ systemPrompt: trimmedPrompt })
|
||||||
|
goNext()
|
||||||
}
|
}
|
||||||
useKeybinding("confirm:no", goBack, t0);
|
|
||||||
let t1;
|
return (
|
||||||
if ($[1] !== systemPrompt) {
|
<WizardDialogLayout
|
||||||
t1 = async () => {
|
subtitle="System prompt"
|
||||||
const result = await editPromptInEditor(systemPrompt);
|
footerText={
|
||||||
if (result.content !== null) {
|
<Byline>
|
||||||
setSystemPrompt(result.content);
|
<KeyboardShortcutHint shortcut="Type" action="enter text" />
|
||||||
setCursorOffset(result.content.length);
|
<KeyboardShortcutHint shortcut="Enter" action="continue" />
|
||||||
|
<ConfigurableShortcutHint
|
||||||
|
action="chat:externalEditor"
|
||||||
|
context="Chat"
|
||||||
|
fallback="ctrl+g"
|
||||||
|
description="open in editor"
|
||||||
|
/>
|
||||||
|
<ConfigurableShortcutHint
|
||||||
|
action="confirm:no"
|
||||||
|
context="Settings"
|
||||||
|
fallback="Esc"
|
||||||
|
description="go back"
|
||||||
|
/>
|
||||||
|
</Byline>
|
||||||
}
|
}
|
||||||
};
|
>
|
||||||
$[1] = systemPrompt;
|
<Box flexDirection="column">
|
||||||
$[2] = t1;
|
<Text>Enter the system prompt for your agent:</Text>
|
||||||
} else {
|
<Text dimColor>Be comprehensive for best results</Text>
|
||||||
t1 = $[2];
|
|
||||||
}
|
<Box marginTop={1}>
|
||||||
const handleExternalEditor = t1;
|
<TextInput
|
||||||
let t2;
|
value={systemPrompt}
|
||||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
onChange={setSystemPrompt}
|
||||||
t2 = {
|
onSubmit={handleSubmit}
|
||||||
context: "Chat"
|
placeholder="You are a helpful code reviewer who..."
|
||||||
};
|
columns={80}
|
||||||
$[3] = t2;
|
cursorOffset={cursorOffset}
|
||||||
} else {
|
onChangeCursorOffset={setCursorOffset}
|
||||||
t2 = $[3];
|
focus
|
||||||
}
|
showCursor
|
||||||
useKeybinding("chat:externalEditor", handleExternalEditor, t2);
|
/>
|
||||||
let t3;
|
</Box>
|
||||||
if ($[4] !== goNext || $[5] !== systemPrompt || $[6] !== updateWizardData) {
|
|
||||||
t3 = () => {
|
{error && (
|
||||||
const trimmedPrompt = systemPrompt.trim();
|
<Box marginTop={1}>
|
||||||
if (!trimmedPrompt) {
|
<Text color="error">{error}</Text>
|
||||||
setError("System prompt is required");
|
</Box>
|
||||||
return;
|
)}
|
||||||
}
|
</Box>
|
||||||
setError(null);
|
</WizardDialogLayout>
|
||||||
updateWizardData({
|
)
|
||||||
systemPrompt: trimmedPrompt
|
|
||||||
});
|
|
||||||
goNext();
|
|
||||||
};
|
|
||||||
$[4] = goNext;
|
|
||||||
$[5] = systemPrompt;
|
|
||||||
$[6] = updateWizardData;
|
|
||||||
$[7] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[7];
|
|
||||||
}
|
|
||||||
const handleSubmit = t3;
|
|
||||||
let t4;
|
|
||||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t4 = <Byline><KeyboardShortcutHint shortcut="Type" action="enter text" /><KeyboardShortcutHint shortcut="Enter" action="continue" /><ConfigurableShortcutHint action="chat:externalEditor" context="Chat" fallback="ctrl+g" description="open in editor" /><ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="go back" /></Byline>;
|
|
||||||
$[8] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[8];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
let t6;
|
|
||||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t5 = <Text>Enter the system prompt for your agent:</Text>;
|
|
||||||
t6 = <Text dimColor={true}>Be comprehensive for best results</Text>;
|
|
||||||
$[9] = t5;
|
|
||||||
$[10] = t6;
|
|
||||||
} else {
|
|
||||||
t5 = $[9];
|
|
||||||
t6 = $[10];
|
|
||||||
}
|
|
||||||
let t7;
|
|
||||||
if ($[11] !== cursorOffset || $[12] !== handleSubmit || $[13] !== systemPrompt) {
|
|
||||||
t7 = <Box marginTop={1}><TextInput value={systemPrompt} onChange={setSystemPrompt} onSubmit={handleSubmit} placeholder="You are a helpful code reviewer who..." columns={80} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus={true} showCursor={true} /></Box>;
|
|
||||||
$[11] = cursorOffset;
|
|
||||||
$[12] = handleSubmit;
|
|
||||||
$[13] = systemPrompt;
|
|
||||||
$[14] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[14];
|
|
||||||
}
|
|
||||||
let t8;
|
|
||||||
if ($[15] !== error) {
|
|
||||||
t8 = error && <Box marginTop={1}><Text color="error">{error}</Text></Box>;
|
|
||||||
$[15] = error;
|
|
||||||
$[16] = t8;
|
|
||||||
} else {
|
|
||||||
t8 = $[16];
|
|
||||||
}
|
|
||||||
let t9;
|
|
||||||
if ($[17] !== t7 || $[18] !== t8) {
|
|
||||||
t9 = <WizardDialogLayout subtitle="System prompt" footerText={t4}><Box flexDirection="column">{t5}{t6}{t7}{t8}</Box></WizardDialogLayout>;
|
|
||||||
$[17] = t7;
|
|
||||||
$[18] = t8;
|
|
||||||
$[19] = t9;
|
|
||||||
} else {
|
|
||||||
t9 = $[19];
|
|
||||||
}
|
|
||||||
return t9;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,52 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { type ReactNode } from 'react'
|
||||||
import React, { type ReactNode } from 'react';
|
import type { Tools } from '../../../../Tool.js'
|
||||||
import type { Tools } from '../../../../Tool.js';
|
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
import { Byline } from '../../../design-system/Byline.js'
|
||||||
import { Byline } from '../../../design-system/Byline.js';
|
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
import { useWizard } from '../../../wizard/index.js'
|
||||||
import { useWizard } from '../../../wizard/index.js';
|
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
import { ToolSelector } from '../../ToolSelector.js'
|
||||||
import { ToolSelector } from '../../ToolSelector.js';
|
import type { AgentWizardData } from '../types.js'
|
||||||
import type { AgentWizardData } from '../types.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tools: Tools;
|
tools: Tools
|
||||||
};
|
}
|
||||||
export function ToolsStep(t0) {
|
|
||||||
const $ = _c(9);
|
export function ToolsStep({ tools }: Props): ReactNode {
|
||||||
const {
|
const { goNext, goBack, updateWizardData, wizardData } =
|
||||||
tools
|
useWizard<AgentWizardData>()
|
||||||
} = t0;
|
|
||||||
const {
|
const handleComplete = (selectedTools: string[] | undefined): void => {
|
||||||
goNext,
|
updateWizardData({ selectedTools })
|
||||||
goBack,
|
goNext()
|
||||||
updateWizardData,
|
}
|
||||||
wizardData
|
|
||||||
} = useWizard();
|
// Pass through undefined to preserve "all tools" semantic
|
||||||
let t1;
|
// ToolSelector will expand it internally for display purposes
|
||||||
if ($[0] !== goNext || $[1] !== updateWizardData) {
|
const initialTools = wizardData.selectedTools
|
||||||
t1 = selectedTools => {
|
|
||||||
updateWizardData({
|
return (
|
||||||
selectedTools
|
<WizardDialogLayout
|
||||||
});
|
subtitle="Select tools"
|
||||||
goNext();
|
footerText={
|
||||||
};
|
<Byline>
|
||||||
$[0] = goNext;
|
<KeyboardShortcutHint shortcut="Enter" action="toggle selection" />
|
||||||
$[1] = updateWizardData;
|
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||||
$[2] = t1;
|
<ConfigurableShortcutHint
|
||||||
} else {
|
action="confirm:no"
|
||||||
t1 = $[2];
|
context="Confirmation"
|
||||||
}
|
fallback="Esc"
|
||||||
const handleComplete = t1;
|
description="go back"
|
||||||
const initialTools = wizardData.selectedTools;
|
/>
|
||||||
let t2;
|
</Byline>
|
||||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
}
|
||||||
t2 = <Byline><KeyboardShortcutHint shortcut="Enter" action="toggle selection" /><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
|
>
|
||||||
$[3] = t2;
|
<ToolSelector
|
||||||
} else {
|
tools={tools}
|
||||||
t2 = $[3];
|
initialTools={initialTools}
|
||||||
}
|
onComplete={handleComplete}
|
||||||
let t3;
|
onCancel={goBack}
|
||||||
if ($[4] !== goBack || $[5] !== handleComplete || $[6] !== initialTools || $[7] !== tools) {
|
/>
|
||||||
t3 = <WizardDialogLayout subtitle="Select tools" footerText={t2}><ToolSelector tools={tools} initialTools={initialTools} onComplete={handleComplete} onCancel={goBack} /></WizardDialogLayout>;
|
</WizardDialogLayout>
|
||||||
$[4] = goBack;
|
)
|
||||||
$[5] = handleComplete;
|
|
||||||
$[6] = initialTools;
|
|
||||||
$[7] = tools;
|
|
||||||
$[8] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[8];
|
|
||||||
}
|
|
||||||
return t3;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +1,83 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { type ReactNode, useState } from 'react'
|
||||||
import React, { type ReactNode, useState } from 'react';
|
import { Box, Text } from '../../../../ink.js'
|
||||||
import { Box, Text } from '../../../../ink.js';
|
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
|
||||||
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
|
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'
|
||||||
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js';
|
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
|
||||||
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
|
import { Byline } from '../../../design-system/Byline.js'
|
||||||
import { Byline } from '../../../design-system/Byline.js';
|
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
|
import TextInput from '../../../TextInput.js'
|
||||||
import TextInput from '../../../TextInput.js';
|
import { useWizard } from '../../../wizard/index.js'
|
||||||
import { useWizard } from '../../../wizard/index.js';
|
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
|
||||||
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
|
import { validateAgentType } from '../../validateAgent.js'
|
||||||
import { validateAgentType } from '../../validateAgent.js';
|
import type { AgentWizardData } from '../types.js'
|
||||||
import type { AgentWizardData } from '../types.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
existingAgents: AgentDefinition[];
|
existingAgents: AgentDefinition[]
|
||||||
};
|
}
|
||||||
export function TypeStep(_props) {
|
|
||||||
const $ = _c(15);
|
export function TypeStep(_props: Props): ReactNode {
|
||||||
const {
|
const { goNext, goBack, updateWizardData, wizardData } =
|
||||||
goNext,
|
useWizard<AgentWizardData>()
|
||||||
goBack,
|
const [agentType, setAgentType] = useState(wizardData.agentType || '')
|
||||||
updateWizardData,
|
const [error, setError] = useState<string | null>(null)
|
||||||
wizardData
|
const [cursorOffset, setCursorOffset] = useState(agentType.length)
|
||||||
} = useWizard();
|
|
||||||
const [agentType, setAgentType] = useState(wizardData.agentType || "");
|
// Handle escape key - Go back to MethodStep
|
||||||
const [error, setError] = useState(null);
|
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
|
||||||
const [cursorOffset, setCursorOffset] = useState(agentType.length);
|
useKeybinding('confirm:no', goBack, { context: 'Settings' })
|
||||||
let t0;
|
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
const handleSubmit = (value: string): void => {
|
||||||
t0 = {
|
const trimmedValue = value.trim()
|
||||||
context: "Settings"
|
const validationError = validateAgentType(trimmedValue)
|
||||||
};
|
|
||||||
$[0] = t0;
|
if (validationError) {
|
||||||
} else {
|
setError(validationError)
|
||||||
t0 = $[0];
|
return
|
||||||
}
|
}
|
||||||
useKeybinding("confirm:no", goBack, t0);
|
|
||||||
let t1;
|
setError(null)
|
||||||
if ($[1] !== goNext || $[2] !== updateWizardData) {
|
updateWizardData({ agentType: trimmedValue })
|
||||||
t1 = value => {
|
goNext()
|
||||||
const trimmedValue = value.trim();
|
}
|
||||||
const validationError = validateAgentType(trimmedValue);
|
|
||||||
if (validationError) {
|
return (
|
||||||
setError(validationError);
|
<WizardDialogLayout
|
||||||
return;
|
subtitle="Agent type (identifier)"
|
||||||
}
|
footerText={
|
||||||
setError(null);
|
<Byline>
|
||||||
updateWizardData({
|
<KeyboardShortcutHint shortcut="Type" action="enter text" />
|
||||||
agentType: trimmedValue
|
<KeyboardShortcutHint shortcut="Enter" action="continue" />
|
||||||
});
|
<ConfigurableShortcutHint
|
||||||
goNext();
|
action="confirm:no"
|
||||||
};
|
context="Settings"
|
||||||
$[1] = goNext;
|
fallback="Esc"
|
||||||
$[2] = updateWizardData;
|
description="go back"
|
||||||
$[3] = t1;
|
/>
|
||||||
} else {
|
</Byline>
|
||||||
t1 = $[3];
|
}
|
||||||
}
|
>
|
||||||
const handleSubmit = t1;
|
<Box flexDirection="column">
|
||||||
let t2;
|
<Text>Enter a unique identifier for your agent:</Text>
|
||||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
<Box marginTop={1}>
|
||||||
t2 = <Byline><KeyboardShortcutHint shortcut="Type" action="enter text" /><KeyboardShortcutHint shortcut="Enter" action="continue" /><ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="go back" /></Byline>;
|
<TextInput
|
||||||
$[4] = t2;
|
value={agentType}
|
||||||
} else {
|
onChange={setAgentType}
|
||||||
t2 = $[4];
|
onSubmit={handleSubmit}
|
||||||
}
|
placeholder="e.g., test-runner, tech-lead, etc"
|
||||||
let t3;
|
columns={60}
|
||||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
cursorOffset={cursorOffset}
|
||||||
t3 = <Text>Enter a unique identifier for your agent:</Text>;
|
onChangeCursorOffset={setCursorOffset}
|
||||||
$[5] = t3;
|
focus
|
||||||
} else {
|
showCursor
|
||||||
t3 = $[5];
|
/>
|
||||||
}
|
</Box>
|
||||||
let t4;
|
|
||||||
if ($[6] !== agentType || $[7] !== cursorOffset || $[8] !== handleSubmit) {
|
{error && (
|
||||||
t4 = <Box marginTop={1}><TextInput value={agentType} onChange={setAgentType} onSubmit={handleSubmit} placeholder="e.g., test-runner, tech-lead, etc" columns={60} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus={true} showCursor={true} /></Box>;
|
<Box marginTop={1}>
|
||||||
$[6] = agentType;
|
<Text color="error">{error}</Text>
|
||||||
$[7] = cursorOffset;
|
</Box>
|
||||||
$[8] = handleSubmit;
|
)}
|
||||||
$[9] = t4;
|
</Box>
|
||||||
} else {
|
</WizardDialogLayout>
|
||||||
t4 = $[9];
|
)
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[10] !== error) {
|
|
||||||
t5 = error && <Box marginTop={1}><Text color="error">{error}</Text></Box>;
|
|
||||||
$[10] = error;
|
|
||||||
$[11] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[11];
|
|
||||||
}
|
|
||||||
let t6;
|
|
||||||
if ($[12] !== t4 || $[13] !== t5) {
|
|
||||||
t6 = <WizardDialogLayout subtitle="Agent type (identifier)" footerText={t2}><Box flexDirection="column">{t3}{t4}{t5}</Box></WizardDialogLayout>;
|
|
||||||
$[12] = t4;
|
|
||||||
$[13] = t5;
|
|
||||||
$[14] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[14];
|
|
||||||
}
|
|
||||||
return t6;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { Children, isValidElement } from 'react'
|
||||||
import React, { Children, isValidElement } from 'react';
|
import { Text } from '../../ink.js'
|
||||||
import { Text } from '../../ink.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/** The items to join with a middot separator */
|
/** The items to join with a middot separator */
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Joins children with a middot separator (" · ") for inline metadata display.
|
* Joins children with a middot separator (" · ") for inline metadata display.
|
||||||
@@ -34,43 +34,24 @@ type Props = {
|
|||||||
* </Text>
|
* </Text>
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function Byline(t0) {
|
export function Byline({ children }: Props): React.ReactNode {
|
||||||
const $ = _c(5);
|
// Children.toArray already filters out null, undefined, and booleans
|
||||||
const {
|
const validChildren = Children.toArray(children)
|
||||||
children
|
|
||||||
} = t0;
|
if (validChildren.length === 0) {
|
||||||
let t1;
|
return null
|
||||||
let t2;
|
|
||||||
if ($[0] !== children) {
|
|
||||||
t2 = Symbol.for("react.early_return_sentinel");
|
|
||||||
bb0: {
|
|
||||||
const validChildren = Children.toArray(children);
|
|
||||||
if (validChildren.length === 0) {
|
|
||||||
t2 = null;
|
|
||||||
break bb0;
|
|
||||||
}
|
|
||||||
t1 = validChildren.map(_temp);
|
|
||||||
}
|
|
||||||
$[0] = children;
|
|
||||||
$[1] = t1;
|
|
||||||
$[2] = t2;
|
|
||||||
} else {
|
|
||||||
t1 = $[1];
|
|
||||||
t2 = $[2];
|
|
||||||
}
|
}
|
||||||
if (t2 !== Symbol.for("react.early_return_sentinel")) {
|
|
||||||
return t2;
|
return (
|
||||||
}
|
<>
|
||||||
let t3;
|
{validChildren.map((child, index) => (
|
||||||
if ($[3] !== t1) {
|
<React.Fragment
|
||||||
t3 = <>{t1}</>;
|
key={isValidElement(child) ? (child.key ?? index) : index}
|
||||||
$[3] = t1;
|
>
|
||||||
$[4] = t3;
|
{index > 0 && <Text dimColor> · </Text>}
|
||||||
} else {
|
{child}
|
||||||
t3 = $[4];
|
</React.Fragment>
|
||||||
}
|
))}
|
||||||
return t3;
|
</>
|
||||||
}
|
)
|
||||||
function _temp(child, index) {
|
|
||||||
return <React.Fragment key={isValidElement(child) ? child.key ?? index : index}>{index > 0 && <Text dimColor={true}> · </Text>}{child}</React.Fragment>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React from 'react'
|
||||||
import React from 'react';
|
import {
|
||||||
import { type ExitState, useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
type ExitState,
|
||||||
import { Box, Text } from '../../ink.js';
|
useExitOnCtrlCDWithKeybindings,
|
||||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
} from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
|
||||||
import type { Theme } from '../../utils/theme.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||||
import { Byline } from './Byline.js';
|
import type { Theme } from '../../utils/theme.js'
|
||||||
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js';
|
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||||
import { Pane } from './Pane.js';
|
import { Byline } from './Byline.js'
|
||||||
|
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'
|
||||||
|
import { Pane } from './Pane.js'
|
||||||
|
|
||||||
type DialogProps = {
|
type DialogProps = {
|
||||||
title: React.ReactNode;
|
title: React.ReactNode
|
||||||
subtitle?: React.ReactNode;
|
subtitle?: React.ReactNode
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
onCancel: () => void;
|
onCancel: () => void
|
||||||
color?: keyof Theme;
|
color?: keyof Theme
|
||||||
hideInputGuide?: boolean;
|
hideInputGuide?: boolean
|
||||||
hideBorder?: boolean;
|
hideBorder?: boolean
|
||||||
/** Custom input guide content. Receives exitState for Ctrl+C/D pending display. */
|
/** Custom input guide content. Receives exitState for Ctrl+C/D pending display. */
|
||||||
inputGuide?: (exitState: ExitState) => React.ReactNode;
|
inputGuide?: (exitState: ExitState) => React.ReactNode
|
||||||
/**
|
/**
|
||||||
* Controls whether Dialog's built-in confirm:no (Esc/n) and app:exit/interrupt
|
* Controls whether Dialog's built-in confirm:no (Esc/n) and app:exit/interrupt
|
||||||
* (Ctrl-C/D) keybindings are active. Set to `false` while an embedded text
|
* (Ctrl-C/D) keybindings are active. Set to `false` while an embedded text
|
||||||
@@ -25,113 +28,73 @@ type DialogProps = {
|
|||||||
* consumed by Dialog. TextInput has its own ctrl+c/d handlers (cancel on
|
* consumed by Dialog. TextInput has its own ctrl+c/d handlers (cancel on
|
||||||
* press, delete-forward on ctrl+d with text). Defaults to `true`.
|
* press, delete-forward on ctrl+d with text). Defaults to `true`.
|
||||||
*/
|
*/
|
||||||
isCancelActive?: boolean;
|
isCancelActive?: boolean
|
||||||
};
|
}
|
||||||
export function Dialog(t0) {
|
|
||||||
const $ = _c(27);
|
export function Dialog({
|
||||||
const {
|
title,
|
||||||
title,
|
subtitle,
|
||||||
subtitle,
|
children,
|
||||||
children,
|
onCancel,
|
||||||
onCancel,
|
color = 'permission',
|
||||||
color: t1,
|
hideInputGuide,
|
||||||
hideInputGuide,
|
hideBorder,
|
||||||
hideBorder,
|
inputGuide,
|
||||||
inputGuide,
|
isCancelActive = true,
|
||||||
isCancelActive: t2
|
}: DialogProps): React.ReactNode {
|
||||||
} = t0;
|
const exitState = useExitOnCtrlCDWithKeybindings(
|
||||||
const color = t1 === undefined ? "permission" : t1;
|
undefined,
|
||||||
const isCancelActive = t2 === undefined ? true : t2;
|
undefined,
|
||||||
const exitState = useExitOnCtrlCDWithKeybindings(undefined, undefined, isCancelActive);
|
isCancelActive,
|
||||||
let t3;
|
)
|
||||||
if ($[0] !== isCancelActive) {
|
|
||||||
t3 = {
|
// Use configurable keybinding for ESC to cancel.
|
||||||
context: "Confirmation",
|
// isCancelActive lets consumers (e.g. ElicitationDialog) disable this while
|
||||||
isActive: isCancelActive
|
// an embedded TextInput is focused, so that keys like 'n' reach the field
|
||||||
};
|
// instead of being consumed here.
|
||||||
$[0] = isCancelActive;
|
useKeybinding('confirm:no', onCancel, {
|
||||||
$[1] = t3;
|
context: 'Confirmation',
|
||||||
} else {
|
isActive: isCancelActive,
|
||||||
t3 = $[1];
|
})
|
||||||
}
|
|
||||||
useKeybinding("confirm:no", onCancel, t3);
|
const defaultInputGuide = exitState.pending ? (
|
||||||
let t4;
|
<Text>Press {exitState.keyName} again to exit</Text>
|
||||||
if ($[2] !== exitState.keyName || $[3] !== exitState.pending) {
|
) : (
|
||||||
t4 = exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline><KeyboardShortcutHint shortcut="Enter" action="confirm" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /></Byline>;
|
<Byline>
|
||||||
$[2] = exitState.keyName;
|
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
|
||||||
$[3] = exitState.pending;
|
<ConfigurableShortcutHint
|
||||||
$[4] = t4;
|
action="confirm:no"
|
||||||
} else {
|
context="Confirmation"
|
||||||
t4 = $[4];
|
fallback="Esc"
|
||||||
}
|
description="cancel"
|
||||||
const defaultInputGuide = t4;
|
/>
|
||||||
let t5;
|
</Byline>
|
||||||
if ($[5] !== color || $[6] !== title) {
|
)
|
||||||
t5 = <Text bold={true} color={color}>{title}</Text>;
|
|
||||||
$[5] = color;
|
const content = (
|
||||||
$[6] = title;
|
<>
|
||||||
$[7] = t5;
|
<Box flexDirection="column" gap={1}>
|
||||||
} else {
|
<Box flexDirection="column">
|
||||||
t5 = $[7];
|
<Text bold color={color}>
|
||||||
}
|
{title}
|
||||||
let t6;
|
</Text>
|
||||||
if ($[8] !== subtitle) {
|
{subtitle && <Text dimColor>{subtitle}</Text>}
|
||||||
t6 = subtitle && <Text dimColor={true}>{subtitle}</Text>;
|
</Box>
|
||||||
$[8] = subtitle;
|
{children}
|
||||||
$[9] = t6;
|
</Box>
|
||||||
} else {
|
{!hideInputGuide && (
|
||||||
t6 = $[9];
|
<Box marginTop={1}>
|
||||||
}
|
<Text dimColor italic>
|
||||||
let t7;
|
{inputGuide ? inputGuide(exitState) : defaultInputGuide}
|
||||||
if ($[10] !== t5 || $[11] !== t6) {
|
</Text>
|
||||||
t7 = <Box flexDirection="column">{t5}{t6}</Box>;
|
</Box>
|
||||||
$[10] = t5;
|
)}
|
||||||
$[11] = t6;
|
</>
|
||||||
$[12] = t7;
|
)
|
||||||
} else {
|
|
||||||
t7 = $[12];
|
if (hideBorder) {
|
||||||
}
|
return content
|
||||||
let t8;
|
}
|
||||||
if ($[13] !== children || $[14] !== t7) {
|
|
||||||
t8 = <Box flexDirection="column" gap={1}>{t7}{children}</Box>;
|
return <Pane color={color}>{content}</Pane>
|
||||||
$[13] = children;
|
|
||||||
$[14] = t7;
|
|
||||||
$[15] = t8;
|
|
||||||
} else {
|
|
||||||
t8 = $[15];
|
|
||||||
}
|
|
||||||
let t9;
|
|
||||||
if ($[16] !== defaultInputGuide || $[17] !== exitState || $[18] !== hideInputGuide || $[19] !== inputGuide) {
|
|
||||||
t9 = !hideInputGuide && <Box marginTop={1}><Text dimColor={true} italic={true}>{inputGuide ? inputGuide(exitState) : defaultInputGuide}</Text></Box>;
|
|
||||||
$[16] = defaultInputGuide;
|
|
||||||
$[17] = exitState;
|
|
||||||
$[18] = hideInputGuide;
|
|
||||||
$[19] = inputGuide;
|
|
||||||
$[20] = t9;
|
|
||||||
} else {
|
|
||||||
t9 = $[20];
|
|
||||||
}
|
|
||||||
let t10;
|
|
||||||
if ($[21] !== t8 || $[22] !== t9) {
|
|
||||||
t10 = <>{t8}{t9}</>;
|
|
||||||
$[21] = t8;
|
|
||||||
$[22] = t9;
|
|
||||||
$[23] = t10;
|
|
||||||
} else {
|
|
||||||
t10 = $[23];
|
|
||||||
}
|
|
||||||
const content = t10;
|
|
||||||
if (hideBorder) {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
let t11;
|
|
||||||
if ($[24] !== color || $[25] !== content) {
|
|
||||||
t11 = <Pane color={color}>{content}</Pane>;
|
|
||||||
$[24] = color;
|
|
||||||
$[25] = content;
|
|
||||||
$[26] = t11;
|
|
||||||
} else {
|
|
||||||
t11 = $[26];
|
|
||||||
}
|
|
||||||
return t11;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React from 'react'
|
||||||
import React from 'react';
|
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
import { stringWidth } from '../../ink/stringWidth.js'
|
||||||
import { stringWidth } from '../../ink/stringWidth.js';
|
import { Ansi, Text } from '../../ink.js'
|
||||||
import { Ansi, Text } from '../../ink.js';
|
import type { Theme } from '../../utils/theme.js'
|
||||||
import type { Theme } from '../../utils/theme.js';
|
|
||||||
type DividerProps = {
|
type DividerProps = {
|
||||||
/**
|
/**
|
||||||
* Width of the divider in characters.
|
* Width of the divider in characters.
|
||||||
* Defaults to terminal width.
|
* Defaults to terminal width.
|
||||||
*/
|
*/
|
||||||
width?: number;
|
width?: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Theme color for the divider.
|
* Theme color for the divider.
|
||||||
* If not provided, dimColor is used.
|
* If not provided, dimColor is used.
|
||||||
*/
|
*/
|
||||||
color?: keyof Theme;
|
color?: keyof Theme
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Character to use for the divider line.
|
* Character to use for the divider line.
|
||||||
* @default '─'
|
* @default '─'
|
||||||
*/
|
*/
|
||||||
char?: string;
|
char?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Padding to subtract from the width (e.g., for indentation).
|
* Padding to subtract from the width (e.g., for indentation).
|
||||||
* @default 0
|
* @default 0
|
||||||
*/
|
*/
|
||||||
padding?: number;
|
padding?: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Title shown in the middle of the divider.
|
* Title shown in the middle of the divider.
|
||||||
@@ -37,8 +37,8 @@ type DividerProps = {
|
|||||||
* // ─────────── Title ───────────
|
* // ─────────── Title ───────────
|
||||||
* <Divider title="Title" />
|
* <Divider title="Title" />
|
||||||
*/
|
*/
|
||||||
title?: string;
|
title?: string
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A horizontal divider line.
|
* A horizontal divider line.
|
||||||
@@ -63,86 +63,35 @@ type DividerProps = {
|
|||||||
* // With centered title
|
* // With centered title
|
||||||
* <Divider title="3 new messages" />
|
* <Divider title="3 new messages" />
|
||||||
*/
|
*/
|
||||||
export function Divider(t0) {
|
export function Divider({
|
||||||
const $ = _c(21);
|
width,
|
||||||
const {
|
color,
|
||||||
width,
|
char = '─',
|
||||||
color,
|
padding = 0,
|
||||||
char: t1,
|
title,
|
||||||
padding: t2,
|
}: DividerProps): React.ReactNode {
|
||||||
title
|
const { columns: terminalWidth } = useTerminalSize()
|
||||||
} = t0;
|
const effectiveWidth = Math.max(0, (width ?? terminalWidth) - padding)
|
||||||
const char = t1 === undefined ? "\u2500" : t1;
|
|
||||||
const padding = t2 === undefined ? 0 : t2;
|
|
||||||
const {
|
|
||||||
columns: terminalWidth
|
|
||||||
} = useTerminalSize();
|
|
||||||
const effectiveWidth = Math.max(0, (width ?? terminalWidth) - padding);
|
|
||||||
if (title) {
|
if (title) {
|
||||||
const titleWidth = stringWidth(title) + 2;
|
const titleWidth = stringWidth(title) + 2 // +2 for spaces around title
|
||||||
const sideWidth = Math.max(0, effectiveWidth - titleWidth);
|
const sideWidth = Math.max(0, effectiveWidth - titleWidth)
|
||||||
const leftWidth = Math.floor(sideWidth / 2);
|
const leftWidth = Math.floor(sideWidth / 2)
|
||||||
const rightWidth = sideWidth - leftWidth;
|
const rightWidth = sideWidth - leftWidth
|
||||||
const t3 = !color;
|
return (
|
||||||
let t4;
|
<Text color={color} dimColor={!color}>
|
||||||
if ($[0] !== char || $[1] !== leftWidth) {
|
{char.repeat(leftWidth)}{' '}
|
||||||
t4 = char.repeat(leftWidth);
|
<Text dimColor>
|
||||||
$[0] = char;
|
<Ansi>{title}</Ansi>
|
||||||
$[1] = leftWidth;
|
</Text>{' '}
|
||||||
$[2] = t4;
|
{char.repeat(rightWidth)}
|
||||||
} else {
|
</Text>
|
||||||
t4 = $[2];
|
)
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[3] !== title) {
|
|
||||||
t5 = <Text dimColor={true}><Ansi>{title}</Ansi></Text>;
|
|
||||||
$[3] = title;
|
|
||||||
$[4] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[4];
|
|
||||||
}
|
|
||||||
let t6;
|
|
||||||
if ($[5] !== char || $[6] !== rightWidth) {
|
|
||||||
t6 = char.repeat(rightWidth);
|
|
||||||
$[5] = char;
|
|
||||||
$[6] = rightWidth;
|
|
||||||
$[7] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[7];
|
|
||||||
}
|
|
||||||
let t7;
|
|
||||||
if ($[8] !== color || $[9] !== t3 || $[10] !== t4 || $[11] !== t5 || $[12] !== t6) {
|
|
||||||
t7 = <Text color={color} dimColor={t3}>{t4}{" "}{t5}{" "}{t6}</Text>;
|
|
||||||
$[8] = color;
|
|
||||||
$[9] = t3;
|
|
||||||
$[10] = t4;
|
|
||||||
$[11] = t5;
|
|
||||||
$[12] = t6;
|
|
||||||
$[13] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[13];
|
|
||||||
}
|
|
||||||
return t7;
|
|
||||||
}
|
}
|
||||||
const t3 = !color;
|
|
||||||
let t4;
|
return (
|
||||||
if ($[14] !== char || $[15] !== effectiveWidth) {
|
<Text color={color} dimColor={!color}>
|
||||||
t4 = char.repeat(effectiveWidth);
|
{char.repeat(effectiveWidth)}
|
||||||
$[14] = char;
|
</Text>
|
||||||
$[15] = effectiveWidth;
|
)
|
||||||
$[16] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[16];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[17] !== color || $[18] !== t3 || $[19] !== t4) {
|
|
||||||
t5 = <Text color={color} dimColor={t3}>{t4}</Text>;
|
|
||||||
$[17] = color;
|
|
||||||
$[18] = t3;
|
|
||||||
$[19] = t4;
|
|
||||||
$[20] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[20];
|
|
||||||
}
|
|
||||||
return t5;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,70 +1,73 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { useEffect, useState } from 'react'
|
||||||
import { useEffect, useState } from 'react';
|
import { useSearchInput } from '../../hooks/useSearchInput.js'
|
||||||
import { useSearchInput } from '../../hooks/useSearchInput.js';
|
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
import { clamp } from '../../ink/layout/geometry.js'
|
||||||
import { clamp } from '../../ink/layout/geometry.js';
|
import { Box, Text, useTerminalFocus } from '../../ink.js'
|
||||||
import { Box, Text, useTerminalFocus } from '../../ink.js';
|
import { SearchBox } from '../SearchBox.js'
|
||||||
import { SearchBox } from '../SearchBox.js';
|
import { Byline } from './Byline.js'
|
||||||
import { Byline } from './Byline.js';
|
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js';
|
import { ListItem } from './ListItem.js'
|
||||||
import { ListItem } from './ListItem.js';
|
import { Pane } from './Pane.js'
|
||||||
import { Pane } from './Pane.js';
|
|
||||||
type PickerAction<T> = {
|
type PickerAction<T> = {
|
||||||
/** Hint label shown in the byline, e.g. "mention" → "Tab to mention". */
|
/** Hint label shown in the byline, e.g. "mention" → "Tab to mention". */
|
||||||
action: string;
|
action: string
|
||||||
handler: (item: T) => void;
|
handler: (item: T) => void
|
||||||
};
|
}
|
||||||
|
|
||||||
type Props<T> = {
|
type Props<T> = {
|
||||||
title: string;
|
title: string
|
||||||
placeholder?: string;
|
placeholder?: string
|
||||||
initialQuery?: string;
|
initialQuery?: string
|
||||||
items: readonly T[];
|
items: readonly T[]
|
||||||
getKey: (item: T) => string;
|
getKey: (item: T) => string
|
||||||
/** Keep to one line — preview handles overflow. */
|
/** Keep to one line — preview handles overflow. */
|
||||||
renderItem: (item: T, isFocused: boolean) => React.ReactNode;
|
renderItem: (item: T, isFocused: boolean) => React.ReactNode
|
||||||
renderPreview?: (item: T) => React.ReactNode;
|
renderPreview?: (item: T) => React.ReactNode
|
||||||
/** 'right' keeps hints stable (no bounce), but needs width. */
|
/** 'right' keeps hints stable (no bounce), but needs width. */
|
||||||
previewPosition?: 'bottom' | 'right';
|
previewPosition?: 'bottom' | 'right'
|
||||||
visibleCount?: number;
|
visibleCount?: number
|
||||||
/**
|
/**
|
||||||
* 'up' puts items[0] at the bottom next to the input (atuin-style). Arrows
|
* 'up' puts items[0] at the bottom next to the input (atuin-style). Arrows
|
||||||
* always match screen direction — ↑ walks visually up regardless.
|
* always match screen direction — ↑ walks visually up regardless.
|
||||||
*/
|
*/
|
||||||
direction?: 'down' | 'up';
|
direction?: 'down' | 'up'
|
||||||
/** Caller owns filtering: re-filter on each call and pass new items. */
|
/** Caller owns filtering: re-filter on each call and pass new items. */
|
||||||
onQueryChange: (query: string) => void;
|
onQueryChange: (query: string) => void
|
||||||
/** Enter key. Primary action. */
|
/** Enter key. Primary action. */
|
||||||
onSelect: (item: T) => void;
|
onSelect: (item: T) => void
|
||||||
/**
|
/**
|
||||||
* Tab key. If provided, Tab no longer aliases Enter — it gets its own
|
* Tab key. If provided, Tab no longer aliases Enter — it gets its own
|
||||||
* handler and hint. Shift+Tab falls through to this if onShiftTab is unset.
|
* handler and hint. Shift+Tab falls through to this if onShiftTab is unset.
|
||||||
*/
|
*/
|
||||||
onTab?: PickerAction<T>;
|
onTab?: PickerAction<T>
|
||||||
/** Shift+Tab key. Gets its own hint. */
|
/** Shift+Tab key. Gets its own hint. */
|
||||||
onShiftTab?: PickerAction<T>;
|
onShiftTab?: PickerAction<T>
|
||||||
/**
|
/**
|
||||||
* Fires when the focused item changes (via arrows or when items reset).
|
* Fires when the focused item changes (via arrows or when items reset).
|
||||||
* Useful for async preview loading — keeps I/O out of renderPreview.
|
* Useful for async preview loading — keeps I/O out of renderPreview.
|
||||||
*/
|
*/
|
||||||
onFocus?: (item: T | undefined) => void;
|
onFocus?: (item: T | undefined) => void
|
||||||
onCancel: () => void;
|
onCancel: () => void
|
||||||
/** Shown when items is empty. Caller bakes loading/searching state into this. */
|
/** Shown when items is empty. Caller bakes loading/searching state into this. */
|
||||||
emptyMessage?: string | ((query: string) => string);
|
emptyMessage?: string | ((query: string) => string)
|
||||||
/**
|
/**
|
||||||
* Status line below the list, e.g. "500+ matches" or "42 matches…".
|
* Status line below the list, e.g. "500+ matches" or "42 matches…".
|
||||||
* Caller decides when to show it — pass undefined to hide.
|
* Caller decides when to show it — pass undefined to hide.
|
||||||
*/
|
*/
|
||||||
matchLabel?: string;
|
matchLabel?: string
|
||||||
selectAction?: string;
|
selectAction?: string
|
||||||
extraHints?: React.ReactNode;
|
extraHints?: React.ReactNode
|
||||||
};
|
}
|
||||||
const DEFAULT_VISIBLE = 8;
|
|
||||||
|
const DEFAULT_VISIBLE = 8
|
||||||
// Pane (paddingTop + Divider) + title + 3 gaps + SearchBox (rounded border = 3
|
// Pane (paddingTop + Divider) + title + 3 gaps + SearchBox (rounded border = 3
|
||||||
// rows) + hints. matchLabel adds +1 when present, accounted for separately.
|
// rows) + hints. matchLabel adds +1 when present, accounted for separately.
|
||||||
const CHROME_ROWS = 10;
|
const CHROME_ROWS = 10
|
||||||
const MIN_VISIBLE = 2;
|
const MIN_VISIBLE = 2
|
||||||
|
|
||||||
export function FuzzyPicker<T>({
|
export function FuzzyPicker<T>({
|
||||||
title,
|
title,
|
||||||
placeholder = 'Type to search…',
|
placeholder = 'Type to search…',
|
||||||
@@ -85,117 +88,168 @@ export function FuzzyPicker<T>({
|
|||||||
emptyMessage = 'No results',
|
emptyMessage = 'No results',
|
||||||
matchLabel,
|
matchLabel,
|
||||||
selectAction = 'select',
|
selectAction = 'select',
|
||||||
extraHints
|
extraHints,
|
||||||
}: Props<T>): React.ReactNode {
|
}: Props<T>): React.ReactNode {
|
||||||
const isTerminalFocused = useTerminalFocus();
|
const isTerminalFocused = useTerminalFocus()
|
||||||
const {
|
const { rows, columns } = useTerminalSize()
|
||||||
rows,
|
const [focusedIndex, setFocusedIndex] = useState(0)
|
||||||
columns
|
|
||||||
} = useTerminalSize();
|
|
||||||
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
||||||
|
|
||||||
// Cap visibleCount so the picker never exceeds the terminal height. When it
|
// Cap visibleCount so the picker never exceeds the terminal height. When it
|
||||||
// overflows, each re-render (arrow key, ctrl+p) mis-positions the cursor-up
|
// overflows, each re-render (arrow key, ctrl+p) mis-positions the cursor-up
|
||||||
// by the overflow amount and a previously-drawn line flashes blank.
|
// by the overflow amount and a previously-drawn line flashes blank.
|
||||||
const visibleCount = Math.max(MIN_VISIBLE, Math.min(requestedVisible, rows - CHROME_ROWS - (matchLabel ? 1 : 0)));
|
const visibleCount = Math.max(
|
||||||
|
MIN_VISIBLE,
|
||||||
|
Math.min(requestedVisible, rows - CHROME_ROWS - (matchLabel ? 1 : 0)),
|
||||||
|
)
|
||||||
|
|
||||||
// Full hint row with onTab+onShiftTab is ~100 chars and wraps inconsistently
|
// Full hint row with onTab+onShiftTab is ~100 chars and wraps inconsistently
|
||||||
// below that. Compact mode drops shift+tab and shortens labels.
|
// below that. Compact mode drops shift+tab and shortens labels.
|
||||||
const compact = columns < 120;
|
const compact = columns < 120
|
||||||
|
|
||||||
const step = (delta: 1 | -1) => {
|
const step = (delta: 1 | -1) => {
|
||||||
setFocusedIndex(i => clamp(i + delta, 0, items.length - 1));
|
setFocusedIndex(i => clamp(i + delta, 0, items.length - 1))
|
||||||
};
|
}
|
||||||
|
|
||||||
// onKeyDown fires after useSearchInput's useInput, so onExit must be a
|
// onKeyDown fires after useSearchInput's useInput, so onExit must be a
|
||||||
// no-op — return/downArrow are handled by handleKeyDown below. onCancel
|
// no-op — return/downArrow are handled by handleKeyDown below. onCancel
|
||||||
// still covers escape/ctrl+c/ctrl+d. Backspace-on-empty is disabled so
|
// still covers escape/ctrl+c/ctrl+d. Backspace-on-empty is disabled so
|
||||||
// a held backspace doesn't eject the user from the dialog.
|
// a held backspace doesn't eject the user from the dialog.
|
||||||
const {
|
const { query, cursorOffset } = useSearchInput({
|
||||||
query,
|
|
||||||
cursorOffset
|
|
||||||
} = useSearchInput({
|
|
||||||
isActive: true,
|
isActive: true,
|
||||||
onExit: () => {},
|
onExit: () => {},
|
||||||
onCancel,
|
onCancel,
|
||||||
initialQuery,
|
initialQuery,
|
||||||
backspaceExitsOnEmpty: false
|
backspaceExitsOnEmpty: false,
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'up' || e.ctrl && e.key === 'p') {
|
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation()
|
||||||
step(direction === 'up' ? 1 : -1);
|
step(direction === 'up' ? 1 : -1)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (e.key === 'down' || e.ctrl && e.key === 'n') {
|
if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation()
|
||||||
step(direction === 'up' ? -1 : 1);
|
step(direction === 'up' ? -1 : 1)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (e.key === 'return') {
|
if (e.key === 'return') {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation()
|
||||||
const selected = items[focusedIndex];
|
const selected = items[focusedIndex]
|
||||||
if (selected) onSelect(selected);
|
if (selected) onSelect(selected)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (e.key === 'tab') {
|
if (e.key === 'tab') {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation()
|
||||||
const selected = items[focusedIndex];
|
const selected = items[focusedIndex]
|
||||||
if (!selected) return;
|
if (!selected) return
|
||||||
const tabAction = e.shift ? onShiftTab ?? onTab : onTab;
|
const tabAction = e.shift ? (onShiftTab ?? onTab) : onTab
|
||||||
if (tabAction) {
|
if (tabAction) {
|
||||||
tabAction.handler(selected);
|
tabAction.handler(selected)
|
||||||
} else {
|
} else {
|
||||||
onSelect(selected);
|
onSelect(selected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onQueryChange(query);
|
onQueryChange(query)
|
||||||
setFocusedIndex(0);
|
setFocusedIndex(0)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [query]);
|
}, [query])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFocusedIndex(i => clamp(i, 0, items.length - 1));
|
setFocusedIndex(i => clamp(i, 0, items.length - 1))
|
||||||
}, [items.length]);
|
}, [items.length])
|
||||||
const focused = items[focusedIndex];
|
|
||||||
|
const focused = items[focusedIndex]
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onFocus?.(focused);
|
onFocus?.(focused)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [focused]);
|
}, [focused])
|
||||||
const windowStart = clamp(focusedIndex - visibleCount + 1, 0, items.length - visibleCount);
|
|
||||||
const visible = items.slice(windowStart, windowStart + visibleCount);
|
const windowStart = clamp(
|
||||||
const emptyText = typeof emptyMessage === 'function' ? emptyMessage(query) : emptyMessage;
|
focusedIndex - visibleCount + 1,
|
||||||
const searchBox = <SearchBox query={query} cursorOffset={cursorOffset} placeholder={placeholder} isFocused isTerminalFocused={isTerminalFocused} />;
|
0,
|
||||||
const listBlock = <List visible={visible} windowStart={windowStart} visibleCount={visibleCount} total={items.length} focusedIndex={focusedIndex} direction={direction} getKey={getKey} renderItem={renderItem} emptyText={emptyText} />;
|
items.length - visibleCount,
|
||||||
const preview = renderPreview && focused ? <Box flexDirection="column" flexGrow={1}>
|
)
|
||||||
|
const visible = items.slice(windowStart, windowStart + visibleCount)
|
||||||
|
|
||||||
|
const emptyText =
|
||||||
|
typeof emptyMessage === 'function' ? emptyMessage(query) : emptyMessage
|
||||||
|
|
||||||
|
const searchBox = (
|
||||||
|
<SearchBox
|
||||||
|
query={query}
|
||||||
|
cursorOffset={cursorOffset}
|
||||||
|
placeholder={placeholder}
|
||||||
|
isFocused
|
||||||
|
isTerminalFocused={isTerminalFocused}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
const listBlock = (
|
||||||
|
<List
|
||||||
|
visible={visible}
|
||||||
|
windowStart={windowStart}
|
||||||
|
visibleCount={visibleCount}
|
||||||
|
total={items.length}
|
||||||
|
focusedIndex={focusedIndex}
|
||||||
|
direction={direction}
|
||||||
|
getKey={getKey}
|
||||||
|
renderItem={renderItem}
|
||||||
|
emptyText={emptyText}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
renderPreview && focused ? (
|
||||||
|
<Box flexDirection="column" flexGrow={1}>
|
||||||
{renderPreview(focused)}
|
{renderPreview(focused)}
|
||||||
</Box> : null;
|
</Box>
|
||||||
|
) : null
|
||||||
|
|
||||||
// Structure must not depend on preview truthiness — when focused goes
|
// Structure must not depend on preview truthiness — when focused goes
|
||||||
// undefined (e.g. delete clears matches), switching row→fragment would
|
// undefined (e.g. delete clears matches), switching row→fragment would
|
||||||
// change both layout AND gap count, bouncing the searchBox below.
|
// change both layout AND gap count, bouncing the searchBox below.
|
||||||
const listGroup = renderPreview && previewPosition === 'right' ? <Box flexDirection="row" gap={2} height={visibleCount + (matchLabel ? 1 : 0)}>
|
const listGroup =
|
||||||
|
renderPreview && previewPosition === 'right' ? (
|
||||||
|
<Box
|
||||||
|
flexDirection="row"
|
||||||
|
gap={2}
|
||||||
|
height={visibleCount + (matchLabel ? 1 : 0)}
|
||||||
|
>
|
||||||
<Box flexDirection="column" flexShrink={0}>
|
<Box flexDirection="column" flexShrink={0}>
|
||||||
{listBlock}
|
{listBlock}
|
||||||
{matchLabel && <Text dimColor>{matchLabel}</Text>}
|
{matchLabel && <Text dimColor>{matchLabel}</Text>}
|
||||||
</Box>
|
</Box>
|
||||||
{preview ?? <Box flexGrow={1} />}
|
{preview ?? <Box flexGrow={1} />}
|
||||||
</Box> :
|
</Box>
|
||||||
// Box (not fragment) so the outer gap={1} doesn't insert a blank line
|
) : (
|
||||||
// between list/matchLabel/preview — that read as extra space above the
|
// Box (not fragment) so the outer gap={1} doesn't insert a blank line
|
||||||
// prompt in direction='up'.
|
// between list/matchLabel/preview — that read as extra space above the
|
||||||
<Box flexDirection="column">
|
// prompt in direction='up'.
|
||||||
|
<Box flexDirection="column">
|
||||||
{listBlock}
|
{listBlock}
|
||||||
{matchLabel && <Text dimColor>{matchLabel}</Text>}
|
{matchLabel && <Text dimColor>{matchLabel}</Text>}
|
||||||
{preview}
|
{preview}
|
||||||
</Box>;
|
</Box>
|
||||||
const inputAbove = direction !== 'up';
|
)
|
||||||
return <Pane color="permission">
|
|
||||||
<Box flexDirection="column" gap={1} tabIndex={0} autoFocus onKeyDown={handleKeyDown}>
|
const inputAbove = direction !== 'up'
|
||||||
|
return (
|
||||||
|
<Pane color="permission">
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
gap={1}
|
||||||
|
tabIndex={0}
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
<Text bold color="permission">
|
<Text bold color="permission">
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -204,108 +258,93 @@ export function FuzzyPicker<T>({
|
|||||||
{!inputAbove && searchBox}
|
{!inputAbove && searchBox}
|
||||||
<Text dimColor>
|
<Text dimColor>
|
||||||
<Byline>
|
<Byline>
|
||||||
<KeyboardShortcutHint shortcut="↑/↓" action={compact ? 'nav' : 'navigate'} />
|
<KeyboardShortcutHint
|
||||||
<KeyboardShortcutHint shortcut="Enter" action={compact ? firstWord(selectAction) : selectAction} />
|
shortcut="↑/↓"
|
||||||
{onTab && <KeyboardShortcutHint shortcut="Tab" action={onTab.action} />}
|
action={compact ? 'nav' : 'navigate'}
|
||||||
{onShiftTab && !compact && <KeyboardShortcutHint shortcut="shift+tab" action={onShiftTab.action} />}
|
/>
|
||||||
|
<KeyboardShortcutHint
|
||||||
|
shortcut="Enter"
|
||||||
|
action={compact ? firstWord(selectAction) : selectAction}
|
||||||
|
/>
|
||||||
|
{onTab && (
|
||||||
|
<KeyboardShortcutHint shortcut="Tab" action={onTab.action} />
|
||||||
|
)}
|
||||||
|
{onShiftTab && !compact && (
|
||||||
|
<KeyboardShortcutHint
|
||||||
|
shortcut="shift+tab"
|
||||||
|
action={onShiftTab.action}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<KeyboardShortcutHint shortcut="Esc" action="cancel" />
|
<KeyboardShortcutHint shortcut="Esc" action="cancel" />
|
||||||
{extraHints}
|
{extraHints}
|
||||||
</Byline>
|
</Byline>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Pane>;
|
</Pane>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
type ListProps<T> = Pick<Props<T>, 'visibleCount' | 'direction' | 'getKey' | 'renderItem'> & {
|
|
||||||
visible: readonly T[];
|
type ListProps<T> = Pick<
|
||||||
windowStart: number;
|
Props<T>,
|
||||||
total: number;
|
'visibleCount' | 'direction' | 'getKey' | 'renderItem'
|
||||||
focusedIndex: number;
|
> & {
|
||||||
emptyText: string;
|
visible: readonly T[]
|
||||||
};
|
windowStart: number
|
||||||
function List(t0) {
|
total: number
|
||||||
const $ = _c(27);
|
focusedIndex: number
|
||||||
const {
|
emptyText: string
|
||||||
visible,
|
}
|
||||||
windowStart,
|
|
||||||
visibleCount,
|
function List<T>({
|
||||||
total,
|
visible,
|
||||||
focusedIndex,
|
windowStart,
|
||||||
direction,
|
visibleCount,
|
||||||
getKey,
|
total,
|
||||||
renderItem,
|
focusedIndex,
|
||||||
emptyText
|
direction,
|
||||||
} = t0;
|
getKey,
|
||||||
|
renderItem,
|
||||||
|
emptyText,
|
||||||
|
}: ListProps<T>): React.ReactNode {
|
||||||
if (visible.length === 0) {
|
if (visible.length === 0) {
|
||||||
let t1;
|
return (
|
||||||
if ($[0] !== emptyText) {
|
<Box height={visibleCount} flexShrink={0}>
|
||||||
t1 = <Text dimColor={true}>{emptyText}</Text>;
|
<Text dimColor>{emptyText}</Text>
|
||||||
$[0] = emptyText;
|
</Box>
|
||||||
$[1] = t1;
|
)
|
||||||
} else {
|
|
||||||
t1 = $[1];
|
|
||||||
}
|
|
||||||
let t2;
|
|
||||||
if ($[2] !== t1 || $[3] !== visibleCount) {
|
|
||||||
t2 = <Box height={visibleCount} flexShrink={0}>{t1}</Box>;
|
|
||||||
$[2] = t1;
|
|
||||||
$[3] = visibleCount;
|
|
||||||
$[4] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[4];
|
|
||||||
}
|
|
||||||
return t2;
|
|
||||||
}
|
}
|
||||||
let t1;
|
|
||||||
if ($[5] !== direction || $[6] !== focusedIndex || $[7] !== getKey || $[8] !== renderItem || $[9] !== total || $[10] !== visible || $[11] !== visibleCount || $[12] !== windowStart) {
|
const rows = visible.map((item, i) => {
|
||||||
let t2;
|
const actualIndex = windowStart + i
|
||||||
if ($[14] !== direction || $[15] !== focusedIndex || $[16] !== getKey || $[17] !== renderItem || $[18] !== total || $[19] !== visible.length || $[20] !== visibleCount || $[21] !== windowStart) {
|
const isFocused = actualIndex === focusedIndex
|
||||||
t2 = (item, i) => {
|
const atLowEdge = i === 0 && windowStart > 0
|
||||||
const actualIndex = windowStart + i;
|
const atHighEdge =
|
||||||
const isFocused = actualIndex === focusedIndex;
|
i === visible.length - 1 && windowStart + visibleCount! < total
|
||||||
const atLowEdge = i === 0 && windowStart > 0;
|
return (
|
||||||
const atHighEdge = i === visible.length - 1 && windowStart + visibleCount < total;
|
<ListItem
|
||||||
return <ListItem key={getKey(item)} isFocused={isFocused} showScrollUp={direction === "up" ? atHighEdge : atLowEdge} showScrollDown={direction === "up" ? atLowEdge : atHighEdge} styled={false}>{renderItem(item, isFocused)}</ListItem>;
|
key={getKey(item)}
|
||||||
};
|
isFocused={isFocused}
|
||||||
$[14] = direction;
|
showScrollUp={direction === 'up' ? atHighEdge : atLowEdge}
|
||||||
$[15] = focusedIndex;
|
showScrollDown={direction === 'up' ? atLowEdge : atHighEdge}
|
||||||
$[16] = getKey;
|
styled={false}
|
||||||
$[17] = renderItem;
|
>
|
||||||
$[18] = total;
|
{renderItem(item, isFocused)}
|
||||||
$[19] = visible.length;
|
</ListItem>
|
||||||
$[20] = visibleCount;
|
)
|
||||||
$[21] = windowStart;
|
})
|
||||||
$[22] = t2;
|
|
||||||
} else {
|
return (
|
||||||
t2 = $[22];
|
<Box
|
||||||
}
|
height={visibleCount}
|
||||||
t1 = visible.map(t2);
|
flexShrink={0}
|
||||||
$[5] = direction;
|
flexDirection={direction === 'up' ? 'column-reverse' : 'column'}
|
||||||
$[6] = focusedIndex;
|
>
|
||||||
$[7] = getKey;
|
{rows}
|
||||||
$[8] = renderItem;
|
</Box>
|
||||||
$[9] = total;
|
)
|
||||||
$[10] = visible;
|
|
||||||
$[11] = visibleCount;
|
|
||||||
$[12] = windowStart;
|
|
||||||
$[13] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[13];
|
|
||||||
}
|
|
||||||
const rows = t1;
|
|
||||||
const t2 = direction === "up" ? "column-reverse" : "column";
|
|
||||||
let t3;
|
|
||||||
if ($[23] !== rows || $[24] !== t2 || $[25] !== visibleCount) {
|
|
||||||
t3 = <Box height={visibleCount} flexShrink={0} flexDirection={t2}>{rows}</Box>;
|
|
||||||
$[23] = rows;
|
|
||||||
$[24] = t2;
|
|
||||||
$[25] = visibleCount;
|
|
||||||
$[26] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[26];
|
|
||||||
}
|
|
||||||
return t3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function firstWord(s: string): string {
|
function firstWord(s: string): string {
|
||||||
const i = s.indexOf(' ');
|
const i = s.indexOf(' ')
|
||||||
return i === -1 ? s : s.slice(0, i);
|
return i === -1 ? s : s.slice(0, i)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React from 'react'
|
||||||
import React from 'react';
|
import Text from '../../ink/components/Text.js'
|
||||||
import Text from '../../ink/components/Text.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/** The key or chord to display (e.g., "ctrl+o", "Enter", "↑/↓") */
|
/** The key or chord to display (e.g., "ctrl+o", "Enter", "↑/↓") */
|
||||||
shortcut: string;
|
shortcut: string
|
||||||
/** The action the key performs (e.g., "expand", "select", "navigate") */
|
/** The action the key performs (e.g., "expand", "select", "navigate") */
|
||||||
action: string;
|
action: string
|
||||||
/** Whether to wrap the hint in parentheses. Default: false */
|
/** Whether to wrap the hint in parentheses. Default: false */
|
||||||
parens?: boolean;
|
parens?: boolean
|
||||||
/** Whether to render the shortcut in bold. Default: false */
|
/** Whether to render the shortcut in bold. Default: false */
|
||||||
bold?: boolean;
|
bold?: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a keyboard shortcut hint like "ctrl+o to expand" or "(tab to toggle)"
|
* Renders a keyboard shortcut hint like "ctrl+o to expand" or "(tab to toggle)"
|
||||||
@@ -35,46 +35,24 @@ type Props = {
|
|||||||
* </Byline>
|
* </Byline>
|
||||||
* </Text>
|
* </Text>
|
||||||
*/
|
*/
|
||||||
export function KeyboardShortcutHint(t0) {
|
export function KeyboardShortcutHint({
|
||||||
const $ = _c(9);
|
shortcut,
|
||||||
const {
|
action,
|
||||||
shortcut,
|
parens = false,
|
||||||
action,
|
bold = false,
|
||||||
parens: t1,
|
}: Props): React.ReactNode {
|
||||||
bold: t2
|
const shortcutText = bold ? <Text bold>{shortcut}</Text> : shortcut
|
||||||
} = t0;
|
|
||||||
const parens = t1 === undefined ? false : t1;
|
|
||||||
const bold = t2 === undefined ? false : t2;
|
|
||||||
let t3;
|
|
||||||
if ($[0] !== bold || $[1] !== shortcut) {
|
|
||||||
t3 = bold ? <Text bold={true}>{shortcut}</Text> : shortcut;
|
|
||||||
$[0] = bold;
|
|
||||||
$[1] = shortcut;
|
|
||||||
$[2] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[2];
|
|
||||||
}
|
|
||||||
const shortcutText = t3;
|
|
||||||
if (parens) {
|
if (parens) {
|
||||||
let t4;
|
return (
|
||||||
if ($[3] !== action || $[4] !== shortcutText) {
|
<Text>
|
||||||
t4 = <Text>({shortcutText} to {action})</Text>;
|
({shortcutText} to {action})
|
||||||
$[3] = action;
|
</Text>
|
||||||
$[4] = shortcutText;
|
)
|
||||||
$[5] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[5];
|
|
||||||
}
|
|
||||||
return t4;
|
|
||||||
}
|
}
|
||||||
let t4;
|
return (
|
||||||
if ($[6] !== action || $[7] !== shortcutText) {
|
<Text>
|
||||||
t4 = <Text>{shortcutText} to {action}</Text>;
|
{shortcutText} to {action}
|
||||||
$[6] = action;
|
</Text>
|
||||||
$[7] = shortcutText;
|
)
|
||||||
$[8] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[8];
|
|
||||||
}
|
|
||||||
return t4;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import figures from 'figures'
|
||||||
import figures from 'figures';
|
import type { ReactNode } from 'react'
|
||||||
import type { ReactNode } from 'react';
|
import React from 'react'
|
||||||
import React from 'react';
|
import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js'
|
||||||
import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
|
||||||
type ListItemProps = {
|
type ListItemProps = {
|
||||||
/**
|
/**
|
||||||
* Whether this item is currently focused (keyboard selection).
|
* Whether this item is currently focused (keyboard selection).
|
||||||
* Shows the pointer indicator (❯) when true.
|
* Shows the pointer indicator (❯) when true.
|
||||||
*/
|
*/
|
||||||
isFocused: boolean;
|
isFocused: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this item is selected (chosen/checked).
|
* Whether this item is selected (chosen/checked).
|
||||||
* Shows the checkmark indicator (✓) when true.
|
* Shows the checkmark indicator (✓) when true.
|
||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
isSelected?: boolean;
|
isSelected?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The content to display for this item.
|
* The content to display for this item.
|
||||||
*/
|
*/
|
||||||
children: ReactNode;
|
children: ReactNode
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional description text displayed below the main content.
|
* Optional description text displayed below the main content.
|
||||||
*/
|
*/
|
||||||
description?: string;
|
description?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a down arrow indicator instead of pointer (for scroll hints).
|
* Show a down arrow indicator instead of pointer (for scroll hints).
|
||||||
* Only applies when not focused.
|
* Only applies when not focused.
|
||||||
*/
|
*/
|
||||||
showScrollDown?: boolean;
|
showScrollDown?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show an up arrow indicator instead of pointer (for scroll hints).
|
* Show an up arrow indicator instead of pointer (for scroll hints).
|
||||||
* Only applies when not focused.
|
* Only applies when not focused.
|
||||||
*/
|
*/
|
||||||
showScrollUp?: boolean;
|
showScrollUp?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to apply automatic styling to the children based on focus/selection state.
|
* Whether to apply automatic styling to the children based on focus/selection state.
|
||||||
@@ -46,21 +46,21 @@ type ListItemProps = {
|
|||||||
* - When false: children are rendered as-is, allowing custom styling
|
* - When false: children are rendered as-is, allowing custom styling
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
styled?: boolean;
|
styled?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this item is disabled. Disabled items show dimmed text and no indicators.
|
* Whether this item is disabled. Disabled items show dimmed text and no indicators.
|
||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
disabled?: boolean;
|
disabled?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this ListItem should declare the terminal cursor position.
|
* Whether this ListItem should declare the terminal cursor position.
|
||||||
* Set false when a child (e.g. BaseTextInput) declares its own cursor.
|
* Set false when a child (e.g. BaseTextInput) declares its own cursor.
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
declareCursor?: boolean;
|
declareCursor?: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list item component for selection UIs (dropdowns, multi-selects, menus).
|
* A list item component for selection UIs (dropdowns, multi-selects, menus).
|
||||||
@@ -101,143 +101,88 @@ type ListItemProps = {
|
|||||||
* <Text color="claude">Custom styled content</Text>
|
* <Text color="claude">Custom styled content</Text>
|
||||||
* </ListItem>
|
* </ListItem>
|
||||||
*/
|
*/
|
||||||
export function ListItem(t0) {
|
export function ListItem({
|
||||||
const $ = _c(32);
|
isFocused,
|
||||||
const {
|
isSelected = false,
|
||||||
isFocused,
|
children,
|
||||||
isSelected: t1,
|
description,
|
||||||
children,
|
showScrollDown,
|
||||||
description,
|
showScrollUp,
|
||||||
showScrollDown,
|
styled = true,
|
||||||
showScrollUp,
|
disabled = false,
|
||||||
styled: t2,
|
declareCursor,
|
||||||
disabled: t3,
|
}: ListItemProps): React.ReactNode {
|
||||||
declareCursor
|
// Determine which indicator to show
|
||||||
} = t0;
|
function renderIndicator(): ReactNode {
|
||||||
const isSelected = t1 === undefined ? false : t1;
|
if (disabled) {
|
||||||
const styled = t2 === undefined ? true : t2;
|
return <Text> </Text>
|
||||||
const disabled = t3 === undefined ? false : t3;
|
}
|
||||||
let t4;
|
|
||||||
if ($[0] !== disabled || $[1] !== isFocused || $[2] !== showScrollDown || $[3] !== showScrollUp) {
|
if (isFocused) {
|
||||||
t4 = function renderIndicator() {
|
return <Text color="suggestion">{figures.pointer}</Text>
|
||||||
if (disabled) {
|
}
|
||||||
return <Text> </Text>;
|
|
||||||
}
|
if (showScrollDown) {
|
||||||
if (isFocused) {
|
return <Text dimColor>{figures.arrowDown}</Text>
|
||||||
return <Text color="suggestion">{figures.pointer}</Text>;
|
}
|
||||||
}
|
|
||||||
if (showScrollDown) {
|
if (showScrollUp) {
|
||||||
return <Text dimColor={true}>{figures.arrowDown}</Text>;
|
return <Text dimColor>{figures.arrowUp}</Text>
|
||||||
}
|
}
|
||||||
if (showScrollUp) {
|
|
||||||
return <Text dimColor={true}>{figures.arrowUp}</Text>;
|
return <Text> </Text>
|
||||||
}
|
|
||||||
return <Text> </Text>;
|
|
||||||
};
|
|
||||||
$[0] = disabled;
|
|
||||||
$[1] = isFocused;
|
|
||||||
$[2] = showScrollDown;
|
|
||||||
$[3] = showScrollUp;
|
|
||||||
$[4] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[4];
|
|
||||||
}
|
}
|
||||||
const renderIndicator = t4;
|
|
||||||
let t5;
|
// Determine text color based on state
|
||||||
if ($[5] !== disabled || $[6] !== isFocused || $[7] !== isSelected || $[8] !== styled) {
|
function getTextColor(): 'success' | 'suggestion' | 'inactive' | undefined {
|
||||||
const getTextColor = function getTextColor() {
|
if (disabled) {
|
||||||
if (disabled) {
|
return 'inactive'
|
||||||
return "inactive";
|
}
|
||||||
}
|
|
||||||
if (!styled) {
|
if (!styled) {
|
||||||
return;
|
return undefined
|
||||||
}
|
}
|
||||||
if (isSelected) {
|
|
||||||
return "success";
|
if (isSelected) {
|
||||||
}
|
return 'success'
|
||||||
if (isFocused) {
|
}
|
||||||
return "suggestion";
|
|
||||||
}
|
if (isFocused) {
|
||||||
};
|
return 'suggestion'
|
||||||
t5 = getTextColor();
|
}
|
||||||
$[5] = disabled;
|
|
||||||
$[6] = isFocused;
|
return undefined
|
||||||
$[7] = isSelected;
|
|
||||||
$[8] = styled;
|
|
||||||
$[9] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[9];
|
|
||||||
}
|
}
|
||||||
const textColor = t5;
|
|
||||||
const t6 = isFocused && !disabled && declareCursor !== false;
|
const textColor = getTextColor()
|
||||||
let t7;
|
|
||||||
if ($[10] !== t6) {
|
// Park the native terminal cursor on the pointer indicator so screen
|
||||||
t7 = {
|
// readers / magnifiers track the focused item. (0,0) is the top-left of
|
||||||
line: 0,
|
// this Box, where the pointer renders.
|
||||||
column: 0,
|
const cursorRef = useDeclaredCursor({
|
||||||
active: t6
|
line: 0,
|
||||||
};
|
column: 0,
|
||||||
$[10] = t6;
|
active: isFocused && !disabled && declareCursor !== false,
|
||||||
$[11] = t7;
|
})
|
||||||
} else {
|
|
||||||
t7 = $[11];
|
return (
|
||||||
}
|
<Box ref={cursorRef} flexDirection="column">
|
||||||
const cursorRef = useDeclaredCursor(t7);
|
<Box flexDirection="row" gap={1}>
|
||||||
let t8;
|
{renderIndicator()}
|
||||||
if ($[12] !== renderIndicator) {
|
{styled ? (
|
||||||
t8 = renderIndicator();
|
<Text color={textColor} dimColor={disabled}>
|
||||||
$[12] = renderIndicator;
|
{children}
|
||||||
$[13] = t8;
|
</Text>
|
||||||
} else {
|
) : (
|
||||||
t8 = $[13];
|
children
|
||||||
}
|
)}
|
||||||
let t9;
|
{isSelected && !disabled && <Text color="success">{figures.tick}</Text>}
|
||||||
if ($[14] !== children || $[15] !== disabled || $[16] !== styled || $[17] !== textColor) {
|
</Box>
|
||||||
t9 = styled ? <Text color={textColor} dimColor={disabled}>{children}</Text> : children;
|
{description && (
|
||||||
$[14] = children;
|
<Box paddingLeft={2}>
|
||||||
$[15] = disabled;
|
<Text color="inactive">{description}</Text>
|
||||||
$[16] = styled;
|
</Box>
|
||||||
$[17] = textColor;
|
)}
|
||||||
$[18] = t9;
|
</Box>
|
||||||
} else {
|
)
|
||||||
t9 = $[18];
|
|
||||||
}
|
|
||||||
let t10;
|
|
||||||
if ($[19] !== disabled || $[20] !== isSelected) {
|
|
||||||
t10 = isSelected && !disabled && <Text color="success">{figures.tick}</Text>;
|
|
||||||
$[19] = disabled;
|
|
||||||
$[20] = isSelected;
|
|
||||||
$[21] = t10;
|
|
||||||
} else {
|
|
||||||
t10 = $[21];
|
|
||||||
}
|
|
||||||
let t11;
|
|
||||||
if ($[22] !== t10 || $[23] !== t8 || $[24] !== t9) {
|
|
||||||
t11 = <Box flexDirection="row" gap={1}>{t8}{t9}{t10}</Box>;
|
|
||||||
$[22] = t10;
|
|
||||||
$[23] = t8;
|
|
||||||
$[24] = t9;
|
|
||||||
$[25] = t11;
|
|
||||||
} else {
|
|
||||||
t11 = $[25];
|
|
||||||
}
|
|
||||||
let t12;
|
|
||||||
if ($[26] !== description) {
|
|
||||||
t12 = description && <Box paddingLeft={2}><Text color="inactive">{description}</Text></Box>;
|
|
||||||
$[26] = description;
|
|
||||||
$[27] = t12;
|
|
||||||
} else {
|
|
||||||
t12 = $[27];
|
|
||||||
}
|
|
||||||
let t13;
|
|
||||||
if ($[28] !== cursorRef || $[29] !== t11 || $[30] !== t12) {
|
|
||||||
t13 = <Box ref={cursorRef} flexDirection="column">{t11}{t12}</Box>;
|
|
||||||
$[28] = cursorRef;
|
|
||||||
$[29] = t11;
|
|
||||||
$[30] = t12;
|
|
||||||
$[31] = t13;
|
|
||||||
} else {
|
|
||||||
t13 = $[31];
|
|
||||||
}
|
|
||||||
return t13;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React from 'react'
|
||||||
import React from 'react';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import { Spinner } from '../Spinner.js'
|
||||||
import { Spinner } from '../Spinner.js';
|
|
||||||
type LoadingStateProps = {
|
type LoadingStateProps = {
|
||||||
/**
|
/**
|
||||||
* The loading message to display next to the spinner.
|
* The loading message to display next to the spinner.
|
||||||
*/
|
*/
|
||||||
message: string;
|
message: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the message in bold.
|
* Display the message in bold.
|
||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
bold?: boolean;
|
bold?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the message in dimmed color.
|
* Display the message in dimmed color.
|
||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
dimColor?: boolean;
|
dimColor?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional subtitle displayed below the main message.
|
* Optional subtitle displayed below the main message.
|
||||||
*/
|
*/
|
||||||
subtitle?: string;
|
subtitle?: string
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A spinner with loading message for async operations.
|
* A spinner with loading message for async operations.
|
||||||
@@ -45,49 +45,22 @@ type LoadingStateProps = {
|
|||||||
* subtitle="Fetching your Claude Code sessions..."
|
* subtitle="Fetching your Claude Code sessions..."
|
||||||
* />
|
* />
|
||||||
*/
|
*/
|
||||||
export function LoadingState(t0) {
|
export function LoadingState({
|
||||||
const $ = _c(10);
|
message,
|
||||||
const {
|
bold = false,
|
||||||
message,
|
dimColor = false,
|
||||||
bold: t1,
|
subtitle,
|
||||||
dimColor: t2,
|
}: LoadingStateProps): React.ReactNode {
|
||||||
subtitle
|
return (
|
||||||
} = t0;
|
<Box flexDirection="column">
|
||||||
const bold = t1 === undefined ? false : t1;
|
<Box flexDirection="row">
|
||||||
const dimColor = t2 === undefined ? false : t2;
|
<Spinner />
|
||||||
let t3;
|
<Text bold={bold} dimColor={dimColor}>
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
{' '}
|
||||||
t3 = <Spinner />;
|
{message}
|
||||||
$[0] = t3;
|
</Text>
|
||||||
} else {
|
</Box>
|
||||||
t3 = $[0];
|
{subtitle && <Text dimColor>{subtitle}</Text>}
|
||||||
}
|
</Box>
|
||||||
let t4;
|
)
|
||||||
if ($[1] !== bold || $[2] !== dimColor || $[3] !== message) {
|
|
||||||
t4 = <Box flexDirection="row">{t3}<Text bold={bold} dimColor={dimColor}>{" "}{message}</Text></Box>;
|
|
||||||
$[1] = bold;
|
|
||||||
$[2] = dimColor;
|
|
||||||
$[3] = message;
|
|
||||||
$[4] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[4];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[5] !== subtitle) {
|
|
||||||
t5 = subtitle && <Text dimColor={true}>{subtitle}</Text>;
|
|
||||||
$[5] = subtitle;
|
|
||||||
$[6] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[6];
|
|
||||||
}
|
|
||||||
let t6;
|
|
||||||
if ($[7] !== t4 || $[8] !== t5) {
|
|
||||||
t6 = <Box flexDirection="column">{t4}{t5}</Box>;
|
|
||||||
$[7] = t4;
|
|
||||||
$[8] = t5;
|
|
||||||
$[9] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[9];
|
|
||||||
}
|
|
||||||
return t6;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React from 'react'
|
||||||
import React from 'react';
|
import { useIsInsideModal } from '../../context/modalContext.js'
|
||||||
import { useIsInsideModal } from '../../context/modalContext.js';
|
import { Box } from '../../ink.js'
|
||||||
import { Box } from '../../ink.js';
|
import type { Theme } from '../../utils/theme.js'
|
||||||
import type { Theme } from '../../utils/theme.js';
|
import { Divider } from './Divider.js'
|
||||||
import { Divider } from './Divider.js';
|
|
||||||
type PaneProps = {
|
type PaneProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
/**
|
/**
|
||||||
* Theme color for the top border line.
|
* Theme color for the top border line.
|
||||||
*/
|
*/
|
||||||
color?: keyof Theme;
|
color?: keyof Theme
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A pane — a region of the terminal that appears below the REPL prompt,
|
* A pane — a region of the terminal that appears below the REPL prompt,
|
||||||
@@ -30,47 +30,28 @@ type PaneProps = {
|
|||||||
* <Tabs title="Sandbox:">...</Tabs>
|
* <Tabs title="Sandbox:">...</Tabs>
|
||||||
* </Pane>
|
* </Pane>
|
||||||
*/
|
*/
|
||||||
export function Pane(t0) {
|
export function Pane({ children, color }: PaneProps): React.ReactNode {
|
||||||
const $ = _c(9);
|
// When rendered inside FullscreenLayout's modal slot, its ▔ divider IS
|
||||||
const {
|
// the frame. Skip our own Divider (would double-frame) and the extra top
|
||||||
children,
|
// padding. This lets slash-command screens that wrap in Pane (e.g.
|
||||||
color
|
// /model → ModelPicker) route through the modal slot unchanged.
|
||||||
} = t0;
|
|
||||||
if (useIsInsideModal()) {
|
if (useIsInsideModal()) {
|
||||||
let t1;
|
// flexShrink=0: the modal slot's absolute Box has no explicit height
|
||||||
if ($[0] !== children) {
|
// (grows to fit, maxHeight cap). With flexGrow=1, re-renders cause
|
||||||
t1 = <Box flexDirection="column" paddingX={1} flexShrink={0}>{children}</Box>;
|
// yoga to resolve this Box's height to 0 against the undetermined
|
||||||
$[0] = children;
|
// parent — /permissions body blanks on Down arrow. See #23592.
|
||||||
$[1] = t1;
|
return (
|
||||||
} else {
|
<Box flexDirection="column" paddingX={1} flexShrink={0}>
|
||||||
t1 = $[1];
|
{children}
|
||||||
}
|
</Box>
|
||||||
return t1;
|
)
|
||||||
}
|
}
|
||||||
let t1;
|
return (
|
||||||
if ($[2] !== color) {
|
<Box flexDirection="column" paddingTop={1}>
|
||||||
t1 = <Divider color={color} />;
|
<Divider color={color} />
|
||||||
$[2] = color;
|
<Box flexDirection="column" paddingX={2}>
|
||||||
$[3] = t1;
|
{children}
|
||||||
} else {
|
</Box>
|
||||||
t1 = $[3];
|
</Box>
|
||||||
}
|
)
|
||||||
let t2;
|
|
||||||
if ($[4] !== children) {
|
|
||||||
t2 = <Box flexDirection="column" paddingX={2}>{children}</Box>;
|
|
||||||
$[4] = children;
|
|
||||||
$[5] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[5];
|
|
||||||
}
|
|
||||||
let t3;
|
|
||||||
if ($[6] !== t1 || $[7] !== t2) {
|
|
||||||
t3 = <Box flexDirection="column" paddingTop={1}>{t1}{t2}</Box>;
|
|
||||||
$[6] = t1;
|
|
||||||
$[7] = t2;
|
|
||||||
$[8] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[8];
|
|
||||||
}
|
|
||||||
return t3;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +1,54 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React from 'react'
|
||||||
import React from 'react';
|
import { Text } from '../../ink.js'
|
||||||
import { Text } from '../../ink.js';
|
import type { Theme } from '../../utils/theme.js'
|
||||||
import type { Theme } from '../../utils/theme.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/**
|
/**
|
||||||
* How much progress to display, between 0 and 1 inclusive
|
* How much progress to display, between 0 and 1 inclusive
|
||||||
*/
|
*/
|
||||||
ratio: number; // [0, 1]
|
ratio: number // [0, 1]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How many characters wide to draw the progress bar
|
* How many characters wide to draw the progress bar
|
||||||
*/
|
*/
|
||||||
width: number; // how many characters wide
|
width: number // how many characters wide
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional color for the filled portion of the bar
|
* Optional color for the filled portion of the bar
|
||||||
*/
|
*/
|
||||||
fillColor?: keyof Theme;
|
fillColor?: keyof Theme
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional color for the empty portion of the bar
|
* Optional color for the empty portion of the bar
|
||||||
*/
|
*/
|
||||||
emptyColor?: keyof Theme;
|
emptyColor?: keyof Theme
|
||||||
};
|
}
|
||||||
const BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
|
|
||||||
export function ProgressBar(t0) {
|
const BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']
|
||||||
const $ = _c(13);
|
|
||||||
const {
|
export function ProgressBar({
|
||||||
ratio: inputRatio,
|
ratio: inputRatio,
|
||||||
width,
|
width,
|
||||||
fillColor,
|
fillColor,
|
||||||
emptyColor
|
emptyColor,
|
||||||
} = t0;
|
}: Props): React.ReactNode {
|
||||||
const ratio = Math.min(1, Math.max(0, inputRatio));
|
const ratio = Math.min(1, Math.max(0, inputRatio))
|
||||||
const whole = Math.floor(ratio * width);
|
const whole = Math.floor(ratio * width)
|
||||||
let t1;
|
const segments = [BLOCKS[BLOCKS.length - 1]!.repeat(whole)]
|
||||||
if ($[0] !== whole) {
|
if (whole < width) {
|
||||||
t1 = BLOCKS[BLOCKS.length - 1].repeat(whole);
|
const remainder = ratio * width - whole
|
||||||
$[0] = whole;
|
const middle = Math.floor(remainder * BLOCKS.length)
|
||||||
$[1] = t1;
|
segments.push(BLOCKS[middle]!)
|
||||||
} else {
|
|
||||||
t1 = $[1];
|
const empty = width - whole - 1
|
||||||
}
|
if (empty > 0) {
|
||||||
let segments;
|
segments.push(BLOCKS[0]!.repeat(empty))
|
||||||
if ($[2] !== ratio || $[3] !== t1 || $[4] !== whole || $[5] !== width) {
|
}
|
||||||
segments = [t1];
|
}
|
||||||
if (whole < width) {
|
|
||||||
const remainder = ratio * width - whole;
|
return (
|
||||||
const middle = Math.floor(remainder * BLOCKS.length);
|
<Text color={fillColor} backgroundColor={emptyColor}>
|
||||||
segments.push(BLOCKS[middle]);
|
{segments.join('')}
|
||||||
const empty = width - whole - 1;
|
</Text>
|
||||||
if (empty > 0) {
|
)
|
||||||
let t2;
|
|
||||||
if ($[7] !== empty) {
|
|
||||||
t2 = BLOCKS[0].repeat(empty);
|
|
||||||
$[7] = empty;
|
|
||||||
$[8] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[8];
|
|
||||||
}
|
|
||||||
segments.push(t2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$[2] = ratio;
|
|
||||||
$[3] = t1;
|
|
||||||
$[4] = whole;
|
|
||||||
$[5] = width;
|
|
||||||
$[6] = segments;
|
|
||||||
} else {
|
|
||||||
segments = $[6];
|
|
||||||
}
|
|
||||||
const t2 = segments.join("");
|
|
||||||
let t3;
|
|
||||||
if ($[9] !== emptyColor || $[10] !== fillColor || $[11] !== t2) {
|
|
||||||
t3 = <Text color={fillColor} backgroundColor={emptyColor}>{t2}</Text>;
|
|
||||||
$[9] = emptyColor;
|
|
||||||
$[10] = fillColor;
|
|
||||||
$[11] = t2;
|
|
||||||
$[12] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[12];
|
|
||||||
}
|
|
||||||
return t3;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +1,45 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'
|
||||||
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
import { useTerminalViewport } from '../../ink/hooks/use-terminal-viewport.js'
|
||||||
import { useTerminalViewport } from '../../ink/hooks/use-terminal-viewport.js';
|
import { Box, type DOMElement, measureElement } from '../../ink.js'
|
||||||
import { Box, type DOMElement, measureElement } from '../../ink.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
lock?: 'always' | 'offscreen';
|
lock?: 'always' | 'offscreen'
|
||||||
};
|
}
|
||||||
export function Ratchet(t0) {
|
|
||||||
const $ = _c(10);
|
export function Ratchet({ children, lock = 'always' }: Props): React.ReactNode {
|
||||||
const {
|
const [viewportRef, { isVisible }] = useTerminalViewport()
|
||||||
children,
|
const { rows } = useTerminalSize()
|
||||||
lock: t1
|
const innerRef = useRef<DOMElement | null>(null)
|
||||||
} = t0;
|
const maxHeight = useRef(0)
|
||||||
const lock = t1 === undefined ? "always" : t1;
|
const [minHeight, setMinHeight] = useState(0)
|
||||||
const [viewportRef, t2] = useTerminalViewport();
|
|
||||||
const {
|
const outerRef = useCallback(
|
||||||
isVisible
|
(el: DOMElement | null) => {
|
||||||
} = t2;
|
viewportRef(el)
|
||||||
const {
|
},
|
||||||
rows
|
[viewportRef],
|
||||||
} = useTerminalSize();
|
)
|
||||||
const innerRef = useRef(null);
|
|
||||||
const maxHeight = useRef(0);
|
const engaged = lock === 'always' || !isVisible
|
||||||
const [minHeight, setMinHeight] = useState(0);
|
|
||||||
let t3;
|
useLayoutEffect(() => {
|
||||||
if ($[0] !== viewportRef) {
|
if (!innerRef.current) {
|
||||||
t3 = el => {
|
return
|
||||||
viewportRef(el);
|
}
|
||||||
};
|
const { height } = measureElement(innerRef.current)
|
||||||
$[0] = viewportRef;
|
if (height > maxHeight.current) {
|
||||||
$[1] = t3;
|
maxHeight.current = Math.min(height, rows)
|
||||||
} else {
|
setMinHeight(maxHeight.current)
|
||||||
t3 = $[1];
|
}
|
||||||
}
|
})
|
||||||
const outerRef = t3;
|
|
||||||
const engaged = lock === "always" || !isVisible;
|
return (
|
||||||
let t4;
|
<Box minHeight={engaged ? minHeight : undefined} ref={outerRef}>
|
||||||
if ($[2] !== rows) {
|
<Box ref={innerRef} flexDirection="column">
|
||||||
t4 = () => {
|
{children}
|
||||||
if (!innerRef.current) {
|
</Box>
|
||||||
return;
|
</Box>
|
||||||
}
|
)
|
||||||
const {
|
|
||||||
height
|
|
||||||
} = measureElement(innerRef.current);
|
|
||||||
if (height > maxHeight.current) {
|
|
||||||
maxHeight.current = Math.min(height, rows);
|
|
||||||
setMinHeight(maxHeight.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$[2] = rows;
|
|
||||||
$[3] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[3];
|
|
||||||
}
|
|
||||||
useLayoutEffect(t4);
|
|
||||||
const t5 = engaged ? minHeight : undefined;
|
|
||||||
let t6;
|
|
||||||
if ($[4] !== children) {
|
|
||||||
t6 = <Box ref={innerRef} flexDirection="column">{children}</Box>;
|
|
||||||
$[4] = children;
|
|
||||||
$[5] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[5];
|
|
||||||
}
|
|
||||||
let t7;
|
|
||||||
if ($[6] !== outerRef || $[7] !== t5 || $[8] !== t6) {
|
|
||||||
t7 = <Box minHeight={t5} ref={outerRef}>{t6}</Box>;
|
|
||||||
$[6] = outerRef;
|
|
||||||
$[7] = t5;
|
|
||||||
$[8] = t6;
|
|
||||||
$[9] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[9];
|
|
||||||
}
|
|
||||||
return t7;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import figures from 'figures'
|
||||||
import figures from 'figures';
|
import React from 'react'
|
||||||
import React from 'react';
|
import { Text } from '../../ink.js'
|
||||||
import { Text } from '../../ink.js';
|
|
||||||
type Status = 'success' | 'error' | 'warning' | 'info' | 'pending' | 'loading';
|
type Status = 'success' | 'error' | 'warning' | 'info' | 'pending' | 'loading'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/**
|
/**
|
||||||
* The status to display. Determines both the icon and color.
|
* The status to display. Determines both the icon and color.
|
||||||
@@ -14,42 +15,28 @@ type Props = {
|
|||||||
* - `pending`: Dimmed circle (○)
|
* - `pending`: Dimmed circle (○)
|
||||||
* - `loading`: Dimmed ellipsis (…)
|
* - `loading`: Dimmed ellipsis (…)
|
||||||
*/
|
*/
|
||||||
status: Status;
|
status: Status
|
||||||
/**
|
/**
|
||||||
* Include a trailing space after the icon. Useful when followed by text.
|
* Include a trailing space after the icon. Useful when followed by text.
|
||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
withSpace?: boolean;
|
withSpace?: boolean
|
||||||
};
|
}
|
||||||
const STATUS_CONFIG: Record<Status, {
|
|
||||||
icon: string;
|
const STATUS_CONFIG: Record<
|
||||||
color: 'success' | 'error' | 'warning' | 'suggestion' | undefined;
|
Status,
|
||||||
}> = {
|
{
|
||||||
success: {
|
icon: string
|
||||||
icon: figures.tick,
|
color: 'success' | 'error' | 'warning' | 'suggestion' | undefined
|
||||||
color: 'success'
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
icon: figures.cross,
|
|
||||||
color: 'error'
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
icon: figures.warning,
|
|
||||||
color: 'warning'
|
|
||||||
},
|
|
||||||
info: {
|
|
||||||
icon: figures.info,
|
|
||||||
color: 'suggestion'
|
|
||||||
},
|
|
||||||
pending: {
|
|
||||||
icon: figures.circle,
|
|
||||||
color: undefined
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
icon: '…',
|
|
||||||
color: undefined
|
|
||||||
}
|
}
|
||||||
};
|
> = {
|
||||||
|
success: { icon: figures.tick, color: 'success' },
|
||||||
|
error: { icon: figures.cross, color: 'error' },
|
||||||
|
warning: { icon: figures.warning, color: 'warning' },
|
||||||
|
info: { icon: figures.info, color: 'suggestion' },
|
||||||
|
pending: { icon: figures.circle, color: undefined },
|
||||||
|
loading: { icon: '…', color: undefined },
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a status indicator icon with appropriate color.
|
* Renders a status indicator icon with appropriate color.
|
||||||
@@ -69,26 +56,16 @@ const STATUS_CONFIG: Record<Status, {
|
|||||||
* Waiting for response
|
* Waiting for response
|
||||||
* </Text>
|
* </Text>
|
||||||
*/
|
*/
|
||||||
export function StatusIcon(t0) {
|
export function StatusIcon({
|
||||||
const $ = _c(5);
|
status,
|
||||||
const {
|
withSpace = false,
|
||||||
status,
|
}: Props): React.ReactNode {
|
||||||
withSpace: t1
|
const config = STATUS_CONFIG[status]
|
||||||
} = t0;
|
|
||||||
const withSpace = t1 === undefined ? false : t1;
|
return (
|
||||||
const config = STATUS_CONFIG[status];
|
<Text color={config.color} dimColor={!config.color}>
|
||||||
const t2 = !config.color;
|
{config.icon}
|
||||||
const t3 = withSpace && " ";
|
{withSpace && ' '}
|
||||||
let t4;
|
</Text>
|
||||||
if ($[0] !== config.color || $[1] !== config.icon || $[2] !== t2 || $[3] !== t3) {
|
)
|
||||||
t4 = <Text color={config.color} dimColor={t2}>{config.icon}{t3}</Text>;
|
|
||||||
$[0] = config.color;
|
|
||||||
$[1] = config.icon;
|
|
||||||
$[2] = t2;
|
|
||||||
$[3] = t3;
|
|
||||||
$[4] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[4];
|
|
||||||
}
|
|
||||||
return t4;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,37 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, {
|
||||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
createContext,
|
||||||
import { useIsInsideModal, useModalScrollRef } from '../../context/modalContext.js';
|
useCallback,
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
useContext,
|
||||||
import ScrollBox from '../../ink/components/ScrollBox.js';
|
useEffect,
|
||||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
useState,
|
||||||
import { stringWidth } from '../../ink/stringWidth.js';
|
} from 'react'
|
||||||
import { Box, Text } from '../../ink.js';
|
import {
|
||||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
useIsInsideModal,
|
||||||
import type { Theme } from '../../utils/theme.js';
|
useModalScrollRef,
|
||||||
|
} from '../../context/modalContext.js'
|
||||||
|
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||||
|
import ScrollBox from '../../ink/components/ScrollBox.js'
|
||||||
|
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||||
|
import { stringWidth } from '../../ink/stringWidth.js'
|
||||||
|
import { Box, Text } from '../../ink.js'
|
||||||
|
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||||
|
import type { Theme } from '../../utils/theme.js'
|
||||||
|
|
||||||
type TabsProps = {
|
type TabsProps = {
|
||||||
children: Array<React.ReactElement<TabProps>>;
|
children: Array<React.ReactElement<TabProps>>
|
||||||
title?: string;
|
title?: string
|
||||||
color?: keyof Theme;
|
color?: keyof Theme
|
||||||
defaultTab?: string;
|
defaultTab?: string
|
||||||
hidden?: boolean;
|
hidden?: boolean
|
||||||
useFullWidth?: boolean;
|
useFullWidth?: boolean
|
||||||
/** Controlled mode: current selected tab id/title */
|
/** Controlled mode: current selected tab id/title */
|
||||||
selectedTab?: string;
|
selectedTab?: string
|
||||||
/** Controlled mode: callback when tab changes */
|
/** Controlled mode: callback when tab changes */
|
||||||
onTabChange?: (tabId: string) => void;
|
onTabChange?: (tabId: string) => void
|
||||||
/** Optional banner to display below tabs header */
|
/** Optional banner to display below tabs header */
|
||||||
banner?: React.ReactNode;
|
banner?: React.ReactNode
|
||||||
/** Disable keyboard navigation (e.g. when a child component handles arrow keys) */
|
/** Disable keyboard navigation (e.g. when a child component handles arrow keys) */
|
||||||
disableNavigation?: boolean;
|
disableNavigation?: boolean
|
||||||
/**
|
/**
|
||||||
* Initial focus state for the tab header row. Defaults to true (header
|
* Initial focus state for the tab header row. Defaults to true (header
|
||||||
* focused, nav always works). Keep the default for Select/list content —
|
* focused, nav always works). Keep the default for Select/list content —
|
||||||
@@ -31,28 +40,30 @@ type TabsProps = {
|
|||||||
* content actually binds left/right/tab (e.g. enum cycling), and show a
|
* content actually binds left/right/tab (e.g. enum cycling), and show a
|
||||||
* "↑ tabs" footer hint — without it tabs look broken.
|
* "↑ tabs" footer hint — without it tabs look broken.
|
||||||
*/
|
*/
|
||||||
initialHeaderFocused?: boolean;
|
initialHeaderFocused?: boolean
|
||||||
/**
|
/**
|
||||||
* Fixed height for the content area. When set, all tabs render within the
|
* Fixed height for the content area. When set, all tabs render within the
|
||||||
* same height (overflow hidden) so switching tabs doesn't cause layout
|
* same height (overflow hidden) so switching tabs doesn't cause layout
|
||||||
* shifts. Shorter tabs get whitespace; taller tabs are clipped.
|
* shifts. Shorter tabs get whitespace; taller tabs are clipped.
|
||||||
*/
|
*/
|
||||||
contentHeight?: number;
|
contentHeight?: number
|
||||||
/**
|
/**
|
||||||
* Let Tab/←/→ switch tabs from focused content. Opt-in since some
|
* Let Tab/←/→ switch tabs from focused content. Opt-in since some
|
||||||
* content uses those keys; pass a reactive boolean to cede them when
|
* content uses those keys; pass a reactive boolean to cede them when
|
||||||
* needed. Switching from content focuses the header.
|
* needed. Switching from content focuses the header.
|
||||||
*/
|
*/
|
||||||
navFromContent?: boolean;
|
navFromContent?: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
type TabsContextValue = {
|
type TabsContextValue = {
|
||||||
selectedTab: string | undefined;
|
selectedTab: string | undefined
|
||||||
width: number | undefined;
|
width: number | undefined
|
||||||
headerFocused: boolean;
|
headerFocused: boolean
|
||||||
focusHeader: () => void;
|
focusHeader: () => void
|
||||||
blurHeader: () => void;
|
blurHeader: () => void
|
||||||
registerOptIn: () => () => void;
|
registerOptIn: () => () => void
|
||||||
};
|
}
|
||||||
|
|
||||||
const TabsContext = createContext<TabsContextValue>({
|
const TabsContext = createContext<TabsContextValue>({
|
||||||
selectedTab: undefined,
|
selectedTab: undefined,
|
||||||
width: undefined,
|
width: undefined,
|
||||||
@@ -61,236 +72,248 @@ const TabsContext = createContext<TabsContextValue>({
|
|||||||
headerFocused: false,
|
headerFocused: false,
|
||||||
focusHeader: () => {},
|
focusHeader: () => {},
|
||||||
blurHeader: () => {},
|
blurHeader: () => {},
|
||||||
registerOptIn: () => () => {}
|
registerOptIn: () => () => {},
|
||||||
});
|
})
|
||||||
export function Tabs(t0) {
|
|
||||||
const $ = _c(25);
|
export function Tabs({
|
||||||
const {
|
title,
|
||||||
title,
|
color,
|
||||||
color,
|
defaultTab,
|
||||||
defaultTab,
|
children,
|
||||||
children,
|
hidden,
|
||||||
hidden,
|
useFullWidth,
|
||||||
useFullWidth,
|
selectedTab: controlledSelectedTab,
|
||||||
selectedTab: controlledSelectedTab,
|
onTabChange,
|
||||||
onTabChange,
|
banner,
|
||||||
banner,
|
disableNavigation,
|
||||||
disableNavigation,
|
initialHeaderFocused = true,
|
||||||
initialHeaderFocused: t1,
|
contentHeight,
|
||||||
contentHeight,
|
navFromContent = false,
|
||||||
navFromContent: t2
|
}: TabsProps): React.ReactNode {
|
||||||
} = t0;
|
const { columns: terminalWidth } = useTerminalSize()
|
||||||
const initialHeaderFocused = t1 === undefined ? true : t1;
|
const tabs = children.map(child => [
|
||||||
const navFromContent = t2 === undefined ? false : t2;
|
child.props.id ?? child.props.title,
|
||||||
const {
|
child.props.title,
|
||||||
columns: terminalWidth
|
])
|
||||||
} = useTerminalSize();
|
const defaultTabIndex = defaultTab
|
||||||
const tabs = children.map(_temp);
|
? tabs.findIndex(tab => defaultTab === tab[0])
|
||||||
const defaultTabIndex = defaultTab ? tabs.findIndex(tab => defaultTab === tab[0]) : 0;
|
: 0
|
||||||
const isControlled = controlledSelectedTab !== undefined;
|
|
||||||
const [internalSelectedTab, setInternalSelectedTab] = useState(defaultTabIndex !== -1 ? defaultTabIndex : 0);
|
// Support both controlled and uncontrolled modes
|
||||||
const controlledTabIndex = isControlled ? tabs.findIndex(tab_0 => tab_0[0] === controlledSelectedTab) : -1;
|
const isControlled = controlledSelectedTab !== undefined
|
||||||
const selectedTabIndex = isControlled ? controlledTabIndex !== -1 ? controlledTabIndex : 0 : internalSelectedTab;
|
const [internalSelectedTab, setInternalSelectedTab] = useState(
|
||||||
const modalScrollRef = useModalScrollRef();
|
defaultTabIndex !== -1 ? defaultTabIndex : 0,
|
||||||
const [headerFocused, setHeaderFocused] = useState(initialHeaderFocused);
|
)
|
||||||
let t3;
|
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
// In controlled mode, find the index of the controlled tab
|
||||||
t3 = () => setHeaderFocused(true);
|
const controlledTabIndex = isControlled
|
||||||
$[0] = t3;
|
? tabs.findIndex(tab => tab[0] === controlledSelectedTab)
|
||||||
} else {
|
: -1
|
||||||
t3 = $[0];
|
const selectedTabIndex = isControlled
|
||||||
}
|
? controlledTabIndex !== -1
|
||||||
const focusHeader = t3;
|
? controlledTabIndex
|
||||||
let t4;
|
: 0
|
||||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
: internalSelectedTab
|
||||||
t4 = () => setHeaderFocused(false);
|
|
||||||
$[1] = t4;
|
const modalScrollRef = useModalScrollRef()
|
||||||
} else {
|
|
||||||
t4 = $[1];
|
// Header focus: left/right/tab only switch tabs when the header row is
|
||||||
}
|
// focused. Children with interactive content call focusHeader() (via
|
||||||
const blurHeader = t4;
|
// useTabHeaderFocus) on up-arrow to hand focus back here; down-arrow
|
||||||
const [optInCount, setOptInCount] = useState(0);
|
// returns it. Tabs that never call the hook see no behavior change —
|
||||||
let t5;
|
// initialHeaderFocused defaults to true so nav always works.
|
||||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
const [headerFocused, setHeaderFocused] = useState(initialHeaderFocused)
|
||||||
t5 = () => {
|
const focusHeader = useCallback(() => setHeaderFocused(true), [])
|
||||||
setOptInCount(_temp2);
|
const blurHeader = useCallback(() => setHeaderFocused(false), [])
|
||||||
return () => setOptInCount(_temp3);
|
// Count of mounted children using useTabHeaderFocus(). Down-arrow blur and
|
||||||
};
|
// the ↓ hint only engage when at least one child has opted in — otherwise
|
||||||
$[2] = t5;
|
// pressing down on a legacy tab would strand the user with nav disabled.
|
||||||
} else {
|
const [optInCount, setOptInCount] = useState(0)
|
||||||
t5 = $[2];
|
const registerOptIn = useCallback(() => {
|
||||||
}
|
setOptInCount(n => n + 1)
|
||||||
const registerOptIn = t5;
|
return () => setOptInCount(n => n - 1)
|
||||||
const optedIn = optInCount > 0;
|
}, [])
|
||||||
const handleTabChange = offset => {
|
const optedIn = optInCount > 0
|
||||||
const newIndex = (selectedTabIndex + tabs.length + offset) % tabs.length;
|
|
||||||
const newTabId = tabs[newIndex]?.[0];
|
const handleTabChange = (offset: number) => {
|
||||||
|
const newIndex = (selectedTabIndex + tabs.length + offset) % tabs.length
|
||||||
|
const newTabId = tabs[newIndex]?.[0]
|
||||||
|
|
||||||
if (isControlled && onTabChange && newTabId) {
|
if (isControlled && onTabChange && newTabId) {
|
||||||
onTabChange(newTabId);
|
onTabChange(newTabId)
|
||||||
} else {
|
} else {
|
||||||
setInternalSelectedTab(newIndex);
|
setInternalSelectedTab(newIndex)
|
||||||
}
|
}
|
||||||
setHeaderFocused(true);
|
// Tab switching is a header action — stay focused so the user can keep
|
||||||
};
|
// cycling. The newly mounted tab can blur via its own interaction.
|
||||||
const t6 = !hidden && !disableNavigation && headerFocused;
|
setHeaderFocused(true)
|
||||||
let t7;
|
|
||||||
if ($[3] !== t6) {
|
|
||||||
t7 = {
|
|
||||||
context: "Tabs",
|
|
||||||
isActive: t6
|
|
||||||
};
|
|
||||||
$[3] = t6;
|
|
||||||
$[4] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[4];
|
|
||||||
}
|
}
|
||||||
useKeybindings({
|
|
||||||
"tabs:next": () => handleTabChange(1),
|
useKeybindings(
|
||||||
"tabs:previous": () => handleTabChange(-1)
|
{
|
||||||
}, t7);
|
'tabs:next': () => handleTabChange(1),
|
||||||
let t8;
|
'tabs:previous': () => handleTabChange(-1),
|
||||||
if ($[5] !== headerFocused || $[6] !== hidden || $[7] !== optedIn) {
|
|
||||||
t8 = e => {
|
|
||||||
if (!headerFocused || !optedIn || hidden) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === "down") {
|
|
||||||
e.preventDefault();
|
|
||||||
setHeaderFocused(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$[5] = headerFocused;
|
|
||||||
$[6] = hidden;
|
|
||||||
$[7] = optedIn;
|
|
||||||
$[8] = t8;
|
|
||||||
} else {
|
|
||||||
t8 = $[8];
|
|
||||||
}
|
|
||||||
const handleKeyDown = t8;
|
|
||||||
const t9 = navFromContent && !headerFocused && optedIn && !hidden && !disableNavigation;
|
|
||||||
let t10;
|
|
||||||
if ($[9] !== t9) {
|
|
||||||
t10 = {
|
|
||||||
context: "Tabs",
|
|
||||||
isActive: t9
|
|
||||||
};
|
|
||||||
$[9] = t9;
|
|
||||||
$[10] = t10;
|
|
||||||
} else {
|
|
||||||
t10 = $[10];
|
|
||||||
}
|
|
||||||
useKeybindings({
|
|
||||||
"tabs:next": () => {
|
|
||||||
handleTabChange(1);
|
|
||||||
setHeaderFocused(true);
|
|
||||||
},
|
},
|
||||||
"tabs:previous": () => {
|
{
|
||||||
handleTabChange(-1);
|
context: 'Tabs',
|
||||||
setHeaderFocused(true);
|
isActive: !hidden && !disableNavigation && headerFocused,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// When the header is focused, down-arrow returns focus to content. Only
|
||||||
|
// active when the selected tab has opted in via useTabHeaderFocus() —
|
||||||
|
// legacy tabs have nowhere to return focus to.
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (!headerFocused || !optedIn || hidden) return
|
||||||
|
if (e.key === 'down') {
|
||||||
|
e.preventDefault()
|
||||||
|
setHeaderFocused(false)
|
||||||
}
|
}
|
||||||
}, t10);
|
|
||||||
const titleWidth = title ? stringWidth(title) + 1 : 0;
|
|
||||||
const tabsWidth = tabs.reduce(_temp4, 0);
|
|
||||||
const usedWidth = titleWidth + tabsWidth;
|
|
||||||
const spacerWidth = useFullWidth ? Math.max(0, terminalWidth - usedWidth) : 0;
|
|
||||||
const contentWidth = useFullWidth ? terminalWidth : undefined;
|
|
||||||
const T0 = Box;
|
|
||||||
const t11 = "column";
|
|
||||||
const t12 = 0;
|
|
||||||
const t13 = true;
|
|
||||||
const t14 = modalScrollRef ? 0 : undefined;
|
|
||||||
const t15 = !hidden && <Box flexDirection="row" gap={1} flexShrink={modalScrollRef ? 0 : undefined}>{title !== undefined && <Text bold={true} color={color}>{title}</Text>}{tabs.map((t16, i) => {
|
|
||||||
const [id, title_0] = t16;
|
|
||||||
const isCurrent = selectedTabIndex === i;
|
|
||||||
const hasColorCursor = color && isCurrent && headerFocused;
|
|
||||||
return <Text key={id} backgroundColor={hasColorCursor ? color : undefined} color={hasColorCursor ? "inverseText" : undefined} inverse={isCurrent && !hasColorCursor} bold={isCurrent}>{" "}{title_0}{" "}</Text>;
|
|
||||||
})}{spacerWidth > 0 && <Text>{" ".repeat(spacerWidth)}</Text>}</Box>;
|
|
||||||
let t17;
|
|
||||||
if ($[11] !== children || $[12] !== contentHeight || $[13] !== contentWidth || $[14] !== hidden || $[15] !== modalScrollRef || $[16] !== selectedTabIndex) {
|
|
||||||
t17 = modalScrollRef ? <Box width={contentWidth} marginTop={hidden ? 0 : 1} flexShrink={0}><ScrollBox key={selectedTabIndex} ref={modalScrollRef} flexDirection="column" flexShrink={0}>{children}</ScrollBox></Box> : <Box width={contentWidth} marginTop={hidden ? 0 : 1} height={contentHeight} overflowY={contentHeight !== undefined ? "hidden" : undefined}>{children}</Box>;
|
|
||||||
$[11] = children;
|
|
||||||
$[12] = contentHeight;
|
|
||||||
$[13] = contentWidth;
|
|
||||||
$[14] = hidden;
|
|
||||||
$[15] = modalScrollRef;
|
|
||||||
$[16] = selectedTabIndex;
|
|
||||||
$[17] = t17;
|
|
||||||
} else {
|
|
||||||
t17 = $[17];
|
|
||||||
}
|
}
|
||||||
let t18;
|
|
||||||
if ($[18] !== T0 || $[19] !== banner || $[20] !== handleKeyDown || $[21] !== t14 || $[22] !== t15 || $[23] !== t17) {
|
// Opt-in: same tabs:next/previous actions, active from content. Focuses
|
||||||
t18 = <T0 flexDirection={t11} tabIndex={t12} autoFocus={t13} onKeyDown={handleKeyDown} flexShrink={t14}>{t15}{banner}{t17}</T0>;
|
// the header so subsequent presses cycle via the handler above.
|
||||||
$[18] = T0;
|
useKeybindings(
|
||||||
$[19] = banner;
|
{
|
||||||
$[20] = handleKeyDown;
|
'tabs:next': () => {
|
||||||
$[21] = t14;
|
handleTabChange(1)
|
||||||
$[22] = t15;
|
setHeaderFocused(true)
|
||||||
$[23] = t17;
|
},
|
||||||
$[24] = t18;
|
'tabs:previous': () => {
|
||||||
} else {
|
handleTabChange(-1)
|
||||||
t18 = $[24];
|
setHeaderFocused(true)
|
||||||
}
|
},
|
||||||
return <TabsContext.Provider value={{
|
},
|
||||||
selectedTab: tabs[selectedTabIndex][0],
|
{
|
||||||
width: contentWidth,
|
context: 'Tabs',
|
||||||
headerFocused,
|
isActive:
|
||||||
focusHeader,
|
navFromContent &&
|
||||||
blurHeader,
|
!headerFocused &&
|
||||||
registerOptIn
|
optedIn &&
|
||||||
}}>{t18}</TabsContext.Provider>;
|
!hidden &&
|
||||||
}
|
!disableNavigation,
|
||||||
function _temp4(sum, t0) {
|
},
|
||||||
const [, tabTitle] = t0;
|
)
|
||||||
return sum + (tabTitle ? stringWidth(tabTitle) : 0) + 2 + 1;
|
|
||||||
}
|
// Calculate spacing to fill the available width. No keyboard hint in the
|
||||||
function _temp3(n_0) {
|
// header row — content footers own hints (see useTabHeaderFocus docs).
|
||||||
return n_0 - 1;
|
const titleWidth = title ? stringWidth(title) + 1 : 0 // +1 for gap
|
||||||
}
|
const tabsWidth = tabs.reduce(
|
||||||
function _temp2(n) {
|
(sum, [, tabTitle]) => sum + (tabTitle ? stringWidth(tabTitle) : 0) + 2 + 1, // +2 for padding, +1 for gap
|
||||||
return n + 1;
|
0,
|
||||||
}
|
)
|
||||||
function _temp(child) {
|
const usedWidth = titleWidth + tabsWidth
|
||||||
return [child.props.id ?? child.props.title, child.props.title];
|
const spacerWidth = useFullWidth ? Math.max(0, terminalWidth - usedWidth) : 0
|
||||||
|
|
||||||
|
const contentWidth = useFullWidth ? terminalWidth : undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsContext.Provider
|
||||||
|
value={{
|
||||||
|
selectedTab: tabs[selectedTabIndex]![0],
|
||||||
|
width: contentWidth,
|
||||||
|
headerFocused,
|
||||||
|
focusHeader,
|
||||||
|
blurHeader,
|
||||||
|
registerOptIn,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
tabIndex={0}
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
// flexShrink=0 inside modal slot — the modal's absolute Box has no
|
||||||
|
// explicit height (grows to fit, maxHeight cap), so flexGrow=1 here
|
||||||
|
// resolves to 0 on re-render and the body blanks on Down arrow.
|
||||||
|
// See #23592. Outside modal, leave layout alone.
|
||||||
|
flexShrink={modalScrollRef ? 0 : undefined}
|
||||||
|
>
|
||||||
|
{!hidden && (
|
||||||
|
<Box
|
||||||
|
flexDirection="row"
|
||||||
|
gap={1}
|
||||||
|
flexShrink={modalScrollRef ? 0 : undefined}
|
||||||
|
>
|
||||||
|
{title !== undefined && (
|
||||||
|
<Text bold color={color}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{tabs.map(([id, title], i) => {
|
||||||
|
const isCurrent = selectedTabIndex === i
|
||||||
|
const hasColorCursor = color && isCurrent && headerFocused
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key={id}
|
||||||
|
backgroundColor={hasColorCursor ? color : undefined}
|
||||||
|
color={hasColorCursor ? 'inverseText' : undefined}
|
||||||
|
inverse={isCurrent && !hasColorCursor}
|
||||||
|
bold={isCurrent}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
{title}{' '}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{spacerWidth > 0 && <Text>{' '.repeat(spacerWidth)}</Text>}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{banner}
|
||||||
|
{modalScrollRef ? (
|
||||||
|
// Inside the modal slot: own the ScrollBox here so the tabs
|
||||||
|
// header row above sits OUTSIDE the scroll area — it can never
|
||||||
|
// scroll off. The ref reaches REPL's ScrollKeybindingHandler via
|
||||||
|
// ModalContext. Keyed by selectedTabIndex → remounts on tab
|
||||||
|
// switch, resetting scrollTop to 0 without scrollTo() timing games.
|
||||||
|
<Box width={contentWidth} marginTop={hidden ? 0 : 1} flexShrink={0}>
|
||||||
|
<ScrollBox
|
||||||
|
key={selectedTabIndex}
|
||||||
|
ref={modalScrollRef}
|
||||||
|
flexDirection="column"
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScrollBox>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
width={contentWidth}
|
||||||
|
marginTop={hidden ? 0 : 1}
|
||||||
|
height={contentHeight}
|
||||||
|
overflowY={contentHeight !== undefined ? 'hidden' : undefined}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</TabsContext.Provider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TabProps = {
|
type TabProps = {
|
||||||
title: string;
|
title: string
|
||||||
id?: string;
|
id?: string
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
};
|
|
||||||
export function Tab(t0) {
|
|
||||||
const $ = _c(4);
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
id,
|
|
||||||
children
|
|
||||||
} = t0;
|
|
||||||
const {
|
|
||||||
selectedTab,
|
|
||||||
width
|
|
||||||
} = useContext(TabsContext);
|
|
||||||
const insideModal = useIsInsideModal();
|
|
||||||
if (selectedTab !== (id ?? title)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const t1 = insideModal ? 0 : undefined;
|
|
||||||
let t2;
|
|
||||||
if ($[0] !== children || $[1] !== t1 || $[2] !== width) {
|
|
||||||
t2 = <Box width={width} flexShrink={t1}>{children}</Box>;
|
|
||||||
$[0] = children;
|
|
||||||
$[1] = t1;
|
|
||||||
$[2] = width;
|
|
||||||
$[3] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[3];
|
|
||||||
}
|
|
||||||
return t2;
|
|
||||||
}
|
}
|
||||||
export function useTabsWidth() {
|
|
||||||
const {
|
export function Tab({ title, id, children }: TabProps): React.ReactNode {
|
||||||
width
|
const { selectedTab, width } = useContext(TabsContext)
|
||||||
} = useContext(TabsContext);
|
const insideModal = useIsInsideModal()
|
||||||
return width;
|
if (selectedTab !== (id ?? title)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box width={width} flexShrink={insideModal ? 0 : undefined}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTabsWidth(): number | undefined {
|
||||||
|
const { width } = useContext(TabsContext)
|
||||||
|
return width
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -304,36 +327,13 @@ export function useTabsWidth() {
|
|||||||
* no onUpFromFirstItem to recover. Split the component so the hook only runs
|
* no onUpFromFirstItem to recover. Split the component so the hook only runs
|
||||||
* when the Select renders.
|
* when the Select renders.
|
||||||
*/
|
*/
|
||||||
export function useTabHeaderFocus() {
|
export function useTabHeaderFocus(): {
|
||||||
const $ = _c(6);
|
headerFocused: boolean
|
||||||
const {
|
focusHeader: () => void
|
||||||
headerFocused,
|
blurHeader: () => void
|
||||||
focusHeader,
|
} {
|
||||||
blurHeader,
|
const { headerFocused, focusHeader, blurHeader, registerOptIn } =
|
||||||
registerOptIn
|
useContext(TabsContext)
|
||||||
} = useContext(TabsContext);
|
useEffect(registerOptIn, [registerOptIn])
|
||||||
let t0;
|
return { headerFocused, focusHeader, blurHeader }
|
||||||
if ($[0] !== registerOptIn) {
|
|
||||||
t0 = [registerOptIn];
|
|
||||||
$[0] = registerOptIn;
|
|
||||||
$[1] = t0;
|
|
||||||
} else {
|
|
||||||
t0 = $[1];
|
|
||||||
}
|
|
||||||
useEffect(registerOptIn, t0);
|
|
||||||
let t1;
|
|
||||||
if ($[2] !== blurHeader || $[3] !== focusHeader || $[4] !== headerFocused) {
|
|
||||||
t1 = {
|
|
||||||
headerFocused,
|
|
||||||
focusHeader,
|
|
||||||
blurHeader
|
|
||||||
};
|
|
||||||
$[2] = blurHeader;
|
|
||||||
$[3] = focusHeader;
|
|
||||||
$[4] = headerFocused;
|
|
||||||
$[5] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[5];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,169 +1,160 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import { feature } from 'bun:bundle'
|
||||||
import { feature } from 'bun:bundle';
|
import React, {
|
||||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
createContext,
|
||||||
import useStdin from '../../ink/hooks/use-stdin.js';
|
useContext,
|
||||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
useEffect,
|
||||||
import { getSystemThemeName, type SystemTheme } from '../../utils/systemTheme.js';
|
useMemo,
|
||||||
import type { ThemeName, ThemeSetting } from '../../utils/theme.js';
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import useStdin from '../../ink/hooks/use-stdin.js'
|
||||||
|
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||||
|
import {
|
||||||
|
getSystemThemeName,
|
||||||
|
type SystemTheme,
|
||||||
|
} from '../../utils/systemTheme.js'
|
||||||
|
import type { ThemeName, ThemeSetting } from '../../utils/theme.js'
|
||||||
|
|
||||||
type ThemeContextValue = {
|
type ThemeContextValue = {
|
||||||
/** The saved user preference. May be 'auto'. */
|
/** The saved user preference. May be 'auto'. */
|
||||||
themeSetting: ThemeSetting;
|
themeSetting: ThemeSetting
|
||||||
setThemeSetting: (setting: ThemeSetting) => void;
|
setThemeSetting: (setting: ThemeSetting) => void
|
||||||
setPreviewTheme: (setting: ThemeSetting) => void;
|
setPreviewTheme: (setting: ThemeSetting) => void
|
||||||
savePreview: () => void;
|
savePreview: () => void
|
||||||
cancelPreview: () => void;
|
cancelPreview: () => void
|
||||||
/** The resolved theme to render with. Never 'auto'. */
|
/** The resolved theme to render with. Never 'auto'. */
|
||||||
currentTheme: ThemeName;
|
currentTheme: ThemeName
|
||||||
};
|
}
|
||||||
|
|
||||||
// Non-'auto' default so useTheme() works without a provider (tests, tooling).
|
// Non-'auto' default so useTheme() works without a provider (tests, tooling).
|
||||||
const DEFAULT_THEME: ThemeName = 'dark';
|
const DEFAULT_THEME: ThemeName = 'dark'
|
||||||
|
|
||||||
const ThemeContext = createContext<ThemeContextValue>({
|
const ThemeContext = createContext<ThemeContextValue>({
|
||||||
themeSetting: DEFAULT_THEME,
|
themeSetting: DEFAULT_THEME,
|
||||||
setThemeSetting: () => {},
|
setThemeSetting: () => {},
|
||||||
setPreviewTheme: () => {},
|
setPreviewTheme: () => {},
|
||||||
savePreview: () => {},
|
savePreview: () => {},
|
||||||
cancelPreview: () => {},
|
cancelPreview: () => {},
|
||||||
currentTheme: DEFAULT_THEME
|
currentTheme: DEFAULT_THEME,
|
||||||
});
|
})
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
initialState?: ThemeSetting;
|
initialState?: ThemeSetting
|
||||||
onThemeSave?: (setting: ThemeSetting) => void;
|
onThemeSave?: (setting: ThemeSetting) => void
|
||||||
};
|
}
|
||||||
|
|
||||||
function defaultInitialTheme(): ThemeSetting {
|
function defaultInitialTheme(): ThemeSetting {
|
||||||
return getGlobalConfig().theme;
|
return getGlobalConfig().theme
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultSaveTheme(setting: ThemeSetting): void {
|
function defaultSaveTheme(setting: ThemeSetting): void {
|
||||||
saveGlobalConfig(current => ({
|
saveGlobalConfig(current => ({ ...current, theme: setting }))
|
||||||
...current,
|
|
||||||
theme: setting
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ThemeProvider({
|
export function ThemeProvider({
|
||||||
children,
|
children,
|
||||||
initialState,
|
initialState,
|
||||||
onThemeSave = defaultSaveTheme
|
onThemeSave = defaultSaveTheme,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [themeSetting, setThemeSetting] = useState(initialState ?? defaultInitialTheme);
|
const [themeSetting, setThemeSetting] = useState(
|
||||||
const [previewTheme, setPreviewTheme] = useState<ThemeSetting | null>(null);
|
initialState ?? defaultInitialTheme,
|
||||||
|
)
|
||||||
|
const [previewTheme, setPreviewTheme] = useState<ThemeSetting | null>(null)
|
||||||
|
|
||||||
// Track terminal theme for 'auto' resolution. Seeds from $COLORFGBG (or
|
// Track terminal theme for 'auto' resolution. Seeds from $COLORFGBG (or
|
||||||
// 'dark' if unset); the OSC 11 watcher corrects it on first poll.
|
// 'dark' if unset); the OSC 11 watcher corrects it on first poll.
|
||||||
const [systemTheme, setSystemTheme] = useState<SystemTheme>(() => (initialState ?? themeSetting) === 'auto' ? getSystemThemeName() : 'dark');
|
const [systemTheme, setSystemTheme] = useState<SystemTheme>(() =>
|
||||||
|
(initialState ?? themeSetting) === 'auto' ? getSystemThemeName() : 'dark',
|
||||||
|
)
|
||||||
|
|
||||||
// The setting currently in effect (preview wins while picker is open)
|
// The setting currently in effect (preview wins while picker is open)
|
||||||
const activeSetting = previewTheme ?? themeSetting;
|
const activeSetting = previewTheme ?? themeSetting
|
||||||
const {
|
|
||||||
internal_querier
|
const { internal_querier } = useStdin()
|
||||||
} = useStdin();
|
|
||||||
|
|
||||||
// Watch for live terminal theme changes while 'auto' is active.
|
// Watch for live terminal theme changes while 'auto' is active.
|
||||||
// Positive feature() pattern so the watcher import is dead-code-eliminated
|
// Positive feature() pattern so the watcher import is dead-code-eliminated
|
||||||
// in external builds.
|
// in external builds.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (feature('AUTO_THEME')) {
|
if (feature('AUTO_THEME')) {
|
||||||
if (activeSetting !== 'auto' || !internal_querier) return;
|
if (activeSetting !== 'auto' || !internal_querier) return
|
||||||
let cleanup: (() => void) | undefined;
|
let cleanup: (() => void) | undefined
|
||||||
let cancelled = false;
|
let cancelled = false
|
||||||
void import('../../utils/systemThemeWatcher.js').then(({
|
void import('../../utils/systemThemeWatcher.js').then(
|
||||||
watchSystemTheme
|
({ watchSystemTheme }) => {
|
||||||
}) => {
|
if (cancelled) return
|
||||||
if (cancelled) return;
|
cleanup = watchSystemTheme(internal_querier, setSystemTheme)
|
||||||
cleanup = watchSystemTheme(internal_querier, setSystemTheme);
|
},
|
||||||
});
|
)
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true
|
||||||
cleanup?.();
|
cleanup?.()
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}, [activeSetting, internal_querier]);
|
}, [activeSetting, internal_querier])
|
||||||
const currentTheme: ThemeName = activeSetting === 'auto' ? systemTheme : activeSetting;
|
|
||||||
const value = useMemo<ThemeContextValue>(() => ({
|
const currentTheme: ThemeName =
|
||||||
themeSetting,
|
activeSetting === 'auto' ? systemTheme : activeSetting
|
||||||
setThemeSetting: (newSetting: ThemeSetting) => {
|
|
||||||
setThemeSetting(newSetting);
|
const value = useMemo<ThemeContextValue>(
|
||||||
setPreviewTheme(null);
|
() => ({
|
||||||
// Switching to 'auto' restarts the watcher (activeSetting dep), whose
|
themeSetting,
|
||||||
// first poll fires immediately. Seed from the cache so the OSC
|
setThemeSetting: (newSetting: ThemeSetting) => {
|
||||||
// round-trip doesn't flash the wrong palette.
|
setThemeSetting(newSetting)
|
||||||
if (newSetting === 'auto') {
|
setPreviewTheme(null)
|
||||||
setSystemTheme(getSystemThemeName());
|
// Switching to 'auto' restarts the watcher (activeSetting dep), whose
|
||||||
}
|
// first poll fires immediately. Seed from the cache so the OSC
|
||||||
onThemeSave?.(newSetting);
|
// round-trip doesn't flash the wrong palette.
|
||||||
},
|
if (newSetting === 'auto') {
|
||||||
setPreviewTheme: (newSetting_0: ThemeSetting) => {
|
setSystemTheme(getSystemThemeName())
|
||||||
setPreviewTheme(newSetting_0);
|
}
|
||||||
if (newSetting_0 === 'auto') {
|
onThemeSave?.(newSetting)
|
||||||
setSystemTheme(getSystemThemeName());
|
},
|
||||||
}
|
setPreviewTheme: (newSetting: ThemeSetting) => {
|
||||||
},
|
setPreviewTheme(newSetting)
|
||||||
savePreview: () => {
|
if (newSetting === 'auto') {
|
||||||
if (previewTheme !== null) {
|
setSystemTheme(getSystemThemeName())
|
||||||
setThemeSetting(previewTheme);
|
}
|
||||||
setPreviewTheme(null);
|
},
|
||||||
onThemeSave?.(previewTheme);
|
savePreview: () => {
|
||||||
}
|
if (previewTheme !== null) {
|
||||||
},
|
setThemeSetting(previewTheme)
|
||||||
cancelPreview: () => {
|
setPreviewTheme(null)
|
||||||
if (previewTheme !== null) {
|
onThemeSave?.(previewTheme)
|
||||||
setPreviewTheme(null);
|
}
|
||||||
}
|
},
|
||||||
},
|
cancelPreview: () => {
|
||||||
currentTheme
|
if (previewTheme !== null) {
|
||||||
}), [themeSetting, previewTheme, currentTheme, onThemeSave]);
|
setPreviewTheme(null)
|
||||||
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
}
|
||||||
|
},
|
||||||
|
currentTheme,
|
||||||
|
}),
|
||||||
|
[themeSetting, previewTheme, currentTheme, onThemeSave],
|
||||||
|
)
|
||||||
|
|
||||||
|
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the resolved theme for rendering (never 'auto') and a setter that
|
* Returns the resolved theme for rendering (never 'auto') and a setter that
|
||||||
* accepts any ThemeSetting (including 'auto').
|
* accepts any ThemeSetting (including 'auto').
|
||||||
*/
|
*/
|
||||||
export function useTheme() {
|
export function useTheme(): [ThemeName, (setting: ThemeSetting) => void] {
|
||||||
const $ = _c(3);
|
const { currentTheme, setThemeSetting } = useContext(ThemeContext)
|
||||||
const {
|
return [currentTheme, setThemeSetting]
|
||||||
currentTheme,
|
|
||||||
setThemeSetting
|
|
||||||
} = useContext(ThemeContext);
|
|
||||||
let t0;
|
|
||||||
if ($[0] !== currentTheme || $[1] !== setThemeSetting) {
|
|
||||||
t0 = [currentTheme, setThemeSetting];
|
|
||||||
$[0] = currentTheme;
|
|
||||||
$[1] = setThemeSetting;
|
|
||||||
$[2] = t0;
|
|
||||||
} else {
|
|
||||||
t0 = $[2];
|
|
||||||
}
|
|
||||||
return t0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the raw theme setting as stored in config. Use this in UI that
|
* Returns the raw theme setting as stored in config. Use this in UI that
|
||||||
* needs to show 'auto' as a distinct choice (e.g., ThemePicker).
|
* needs to show 'auto' as a distinct choice (e.g., ThemePicker).
|
||||||
*/
|
*/
|
||||||
export function useThemeSetting() {
|
export function useThemeSetting(): ThemeSetting {
|
||||||
return useContext(ThemeContext).themeSetting;
|
return useContext(ThemeContext).themeSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePreviewTheme() {
|
export function usePreviewTheme() {
|
||||||
const $ = _c(4);
|
const { setPreviewTheme, savePreview, cancelPreview } =
|
||||||
const {
|
useContext(ThemeContext)
|
||||||
setPreviewTheme,
|
return { setPreviewTheme, savePreview, cancelPreview }
|
||||||
savePreview,
|
|
||||||
cancelPreview
|
|
||||||
} = useContext(ThemeContext);
|
|
||||||
let t0;
|
|
||||||
if ($[0] !== cancelPreview || $[1] !== savePreview || $[2] !== setPreviewTheme) {
|
|
||||||
t0 = {
|
|
||||||
setPreviewTheme,
|
|
||||||
savePreview,
|
|
||||||
cancelPreview
|
|
||||||
};
|
|
||||||
$[0] = cancelPreview;
|
|
||||||
$[1] = savePreview;
|
|
||||||
$[2] = setPreviewTheme;
|
|
||||||
$[3] = t0;
|
|
||||||
} else {
|
|
||||||
t0 = $[3];
|
|
||||||
}
|
|
||||||
return t0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,155 +1,112 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { type PropsWithChildren, type Ref } from 'react'
|
||||||
import React, { type PropsWithChildren, type Ref } from 'react';
|
import Box from '../../ink/components/Box.js'
|
||||||
import Box from '../../ink/components/Box.js';
|
import type { DOMElement } from '../../ink/dom.js'
|
||||||
import type { DOMElement } from '../../ink/dom.js';
|
import type { ClickEvent } from '../../ink/events/click-event.js'
|
||||||
import type { ClickEvent } from '../../ink/events/click-event.js';
|
import type { FocusEvent } from '../../ink/events/focus-event.js'
|
||||||
import type { FocusEvent } from '../../ink/events/focus-event.js';
|
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
import type { Color, Styles } from '../../ink/styles.js'
|
||||||
import type { Color, Styles } from '../../ink/styles.js';
|
import { getTheme, type Theme } from '../../utils/theme.js'
|
||||||
import { getTheme, type Theme } from '../../utils/theme.js';
|
import { useTheme } from './ThemeProvider.js'
|
||||||
import { useTheme } from './ThemeProvider.js';
|
|
||||||
|
|
||||||
// Color props that accept theme keys
|
// Color props that accept theme keys
|
||||||
type ThemedColorProps = {
|
type ThemedColorProps = {
|
||||||
readonly borderColor?: keyof Theme | Color;
|
readonly borderColor?: keyof Theme | Color
|
||||||
readonly borderTopColor?: keyof Theme | Color;
|
readonly borderTopColor?: keyof Theme | Color
|
||||||
readonly borderBottomColor?: keyof Theme | Color;
|
readonly borderBottomColor?: keyof Theme | Color
|
||||||
readonly borderLeftColor?: keyof Theme | Color;
|
readonly borderLeftColor?: keyof Theme | Color
|
||||||
readonly borderRightColor?: keyof Theme | Color;
|
readonly borderRightColor?: keyof Theme | Color
|
||||||
readonly backgroundColor?: keyof Theme | Color;
|
readonly backgroundColor?: keyof Theme | Color
|
||||||
};
|
}
|
||||||
|
|
||||||
// Base Styles without color props (they'll be overridden)
|
// Base Styles without color props (they'll be overridden)
|
||||||
type BaseStylesWithoutColors = Omit<Styles, 'textWrap' | 'borderColor' | 'borderTopColor' | 'borderBottomColor' | 'borderLeftColor' | 'borderRightColor' | 'backgroundColor'>;
|
type BaseStylesWithoutColors = Omit<
|
||||||
export type Props = BaseStylesWithoutColors & ThemedColorProps & {
|
Styles,
|
||||||
ref?: Ref<DOMElement>;
|
| 'textWrap'
|
||||||
tabIndex?: number;
|
| 'borderColor'
|
||||||
autoFocus?: boolean;
|
| 'borderTopColor'
|
||||||
onClick?: (event: ClickEvent) => void;
|
| 'borderBottomColor'
|
||||||
onFocus?: (event: FocusEvent) => void;
|
| 'borderLeftColor'
|
||||||
onFocusCapture?: (event: FocusEvent) => void;
|
| 'borderRightColor'
|
||||||
onBlur?: (event: FocusEvent) => void;
|
| 'backgroundColor'
|
||||||
onBlurCapture?: (event: FocusEvent) => void;
|
>
|
||||||
onKeyDown?: (event: KeyboardEvent) => void;
|
|
||||||
onKeyDownCapture?: (event: KeyboardEvent) => void;
|
export type Props = BaseStylesWithoutColors &
|
||||||
onMouseEnter?: () => void;
|
ThemedColorProps & {
|
||||||
onMouseLeave?: () => void;
|
ref?: Ref<DOMElement>
|
||||||
};
|
tabIndex?: number
|
||||||
|
autoFocus?: boolean
|
||||||
|
onClick?: (event: ClickEvent) => void
|
||||||
|
onFocus?: (event: FocusEvent) => void
|
||||||
|
onFocusCapture?: (event: FocusEvent) => void
|
||||||
|
onBlur?: (event: FocusEvent) => void
|
||||||
|
onBlurCapture?: (event: FocusEvent) => void
|
||||||
|
onKeyDown?: (event: KeyboardEvent) => void
|
||||||
|
onKeyDownCapture?: (event: KeyboardEvent) => void
|
||||||
|
onMouseEnter?: () => void
|
||||||
|
onMouseLeave?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a color value that may be a theme key to a raw Color.
|
* Resolves a color value that may be a theme key to a raw Color.
|
||||||
*/
|
*/
|
||||||
function resolveColor(color: keyof Theme | Color | undefined, theme: Theme): Color | undefined {
|
function resolveColor(
|
||||||
if (!color) return undefined;
|
color: keyof Theme | Color | undefined,
|
||||||
|
theme: Theme,
|
||||||
|
): Color | undefined {
|
||||||
|
if (!color) return undefined
|
||||||
// Check if it's a raw color (starts with rgb(, #, ansi256(, or ansi:)
|
// Check if it's a raw color (starts with rgb(, #, ansi256(, or ansi:)
|
||||||
if (color.startsWith('rgb(') || color.startsWith('#') || color.startsWith('ansi256(') || color.startsWith('ansi:')) {
|
if (
|
||||||
return color as Color;
|
color.startsWith('rgb(') ||
|
||||||
|
color.startsWith('#') ||
|
||||||
|
color.startsWith('ansi256(') ||
|
||||||
|
color.startsWith('ansi:')
|
||||||
|
) {
|
||||||
|
return color as Color
|
||||||
}
|
}
|
||||||
// It's a theme key - resolve it
|
// It's a theme key - resolve it
|
||||||
return theme[color as keyof Theme] as Color;
|
return theme[color as keyof Theme] as Color
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Theme-aware Box component that resolves theme color keys to raw colors.
|
* Theme-aware Box component that resolves theme color keys to raw colors.
|
||||||
* This wraps the base Box component with theme resolution for border colors.
|
* This wraps the base Box component with theme resolution for border colors.
|
||||||
*/
|
*/
|
||||||
function ThemedBox(t0) {
|
function ThemedBox({
|
||||||
const $ = _c(33);
|
borderColor,
|
||||||
let backgroundColor;
|
borderTopColor,
|
||||||
let borderBottomColor;
|
borderBottomColor,
|
||||||
let borderColor;
|
borderLeftColor,
|
||||||
let borderLeftColor;
|
borderRightColor,
|
||||||
let borderRightColor;
|
backgroundColor,
|
||||||
let borderTopColor;
|
children,
|
||||||
let children;
|
ref,
|
||||||
let ref;
|
...rest
|
||||||
let rest;
|
}: PropsWithChildren<Props>): React.ReactNode {
|
||||||
if ($[0] !== t0) {
|
const [themeName] = useTheme()
|
||||||
({
|
const theme = getTheme(themeName)
|
||||||
borderColor,
|
|
||||||
borderTopColor,
|
// Resolve theme keys to raw colors
|
||||||
borderBottomColor,
|
const resolvedBorderColor = resolveColor(borderColor, theme)
|
||||||
borderLeftColor,
|
const resolvedBorderTopColor = resolveColor(borderTopColor, theme)
|
||||||
borderRightColor,
|
const resolvedBorderBottomColor = resolveColor(borderBottomColor, theme)
|
||||||
backgroundColor,
|
const resolvedBorderLeftColor = resolveColor(borderLeftColor, theme)
|
||||||
children,
|
const resolvedBorderRightColor = resolveColor(borderRightColor, theme)
|
||||||
ref,
|
const resolvedBackgroundColor = resolveColor(backgroundColor, theme)
|
||||||
...rest
|
|
||||||
} = t0);
|
return (
|
||||||
$[0] = t0;
|
<Box
|
||||||
$[1] = backgroundColor;
|
ref={ref}
|
||||||
$[2] = borderBottomColor;
|
borderColor={resolvedBorderColor}
|
||||||
$[3] = borderColor;
|
borderTopColor={resolvedBorderTopColor}
|
||||||
$[4] = borderLeftColor;
|
borderBottomColor={resolvedBorderBottomColor}
|
||||||
$[5] = borderRightColor;
|
borderLeftColor={resolvedBorderLeftColor}
|
||||||
$[6] = borderTopColor;
|
borderRightColor={resolvedBorderRightColor}
|
||||||
$[7] = children;
|
backgroundColor={resolvedBackgroundColor}
|
||||||
$[8] = ref;
|
{...rest}
|
||||||
$[9] = rest;
|
>
|
||||||
} else {
|
{children}
|
||||||
backgroundColor = $[1];
|
</Box>
|
||||||
borderBottomColor = $[2];
|
)
|
||||||
borderColor = $[3];
|
|
||||||
borderLeftColor = $[4];
|
|
||||||
borderRightColor = $[5];
|
|
||||||
borderTopColor = $[6];
|
|
||||||
children = $[7];
|
|
||||||
ref = $[8];
|
|
||||||
rest = $[9];
|
|
||||||
}
|
|
||||||
const [themeName] = useTheme();
|
|
||||||
let resolvedBorderBottomColor;
|
|
||||||
let resolvedBorderColor;
|
|
||||||
let resolvedBorderLeftColor;
|
|
||||||
let resolvedBorderRightColor;
|
|
||||||
let resolvedBorderTopColor;
|
|
||||||
let t1;
|
|
||||||
if ($[10] !== backgroundColor || $[11] !== borderBottomColor || $[12] !== borderColor || $[13] !== borderLeftColor || $[14] !== borderRightColor || $[15] !== borderTopColor || $[16] !== themeName) {
|
|
||||||
const theme = getTheme(themeName);
|
|
||||||
resolvedBorderColor = resolveColor(borderColor, theme);
|
|
||||||
resolvedBorderTopColor = resolveColor(borderTopColor, theme);
|
|
||||||
resolvedBorderBottomColor = resolveColor(borderBottomColor, theme);
|
|
||||||
resolvedBorderLeftColor = resolveColor(borderLeftColor, theme);
|
|
||||||
resolvedBorderRightColor = resolveColor(borderRightColor, theme);
|
|
||||||
t1 = resolveColor(backgroundColor, theme);
|
|
||||||
$[10] = backgroundColor;
|
|
||||||
$[11] = borderBottomColor;
|
|
||||||
$[12] = borderColor;
|
|
||||||
$[13] = borderLeftColor;
|
|
||||||
$[14] = borderRightColor;
|
|
||||||
$[15] = borderTopColor;
|
|
||||||
$[16] = themeName;
|
|
||||||
$[17] = resolvedBorderBottomColor;
|
|
||||||
$[18] = resolvedBorderColor;
|
|
||||||
$[19] = resolvedBorderLeftColor;
|
|
||||||
$[20] = resolvedBorderRightColor;
|
|
||||||
$[21] = resolvedBorderTopColor;
|
|
||||||
$[22] = t1;
|
|
||||||
} else {
|
|
||||||
resolvedBorderBottomColor = $[17];
|
|
||||||
resolvedBorderColor = $[18];
|
|
||||||
resolvedBorderLeftColor = $[19];
|
|
||||||
resolvedBorderRightColor = $[20];
|
|
||||||
resolvedBorderTopColor = $[21];
|
|
||||||
t1 = $[22];
|
|
||||||
}
|
|
||||||
const resolvedBackgroundColor = t1;
|
|
||||||
let t2;
|
|
||||||
if ($[23] !== children || $[24] !== ref || $[25] !== resolvedBackgroundColor || $[26] !== resolvedBorderBottomColor || $[27] !== resolvedBorderColor || $[28] !== resolvedBorderLeftColor || $[29] !== resolvedBorderRightColor || $[30] !== resolvedBorderTopColor || $[31] !== rest) {
|
|
||||||
t2 = <Box ref={ref} borderColor={resolvedBorderColor} borderTopColor={resolvedBorderTopColor} borderBottomColor={resolvedBorderBottomColor} borderLeftColor={resolvedBorderLeftColor} borderRightColor={resolvedBorderRightColor} backgroundColor={resolvedBackgroundColor} {...rest}>{children}</Box>;
|
|
||||||
$[23] = children;
|
|
||||||
$[24] = ref;
|
|
||||||
$[25] = resolvedBackgroundColor;
|
|
||||||
$[26] = resolvedBorderBottomColor;
|
|
||||||
$[27] = resolvedBorderColor;
|
|
||||||
$[28] = resolvedBorderLeftColor;
|
|
||||||
$[29] = resolvedBorderRightColor;
|
|
||||||
$[30] = resolvedBorderTopColor;
|
|
||||||
$[31] = rest;
|
|
||||||
$[32] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[32];
|
|
||||||
}
|
|
||||||
return t2;
|
|
||||||
}
|
}
|
||||||
export default ThemedBox;
|
|
||||||
|
export default ThemedBox
|
||||||
|
|||||||
@@ -1,123 +1,132 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import type { ReactNode } from 'react'
|
||||||
import type { ReactNode } from 'react';
|
import React, { useContext } from 'react'
|
||||||
import React, { useContext } from 'react';
|
import Text from '../../ink/components/Text.js'
|
||||||
import Text from '../../ink/components/Text.js';
|
import type { Color, Styles } from '../../ink/styles.js'
|
||||||
import type { Color, Styles } from '../../ink/styles.js';
|
import { getTheme, type Theme } from '../../utils/theme.js'
|
||||||
import { getTheme, type Theme } from '../../utils/theme.js';
|
import { useTheme } from './ThemeProvider.js'
|
||||||
import { useTheme } from './ThemeProvider.js';
|
|
||||||
|
|
||||||
/** Colors uncolored ThemedText in the subtree. Precedence: explicit `color` >
|
/** Colors uncolored ThemedText in the subtree. Precedence: explicit `color` >
|
||||||
* this > dimColor. Crosses Box boundaries (Ink's style cascade doesn't). */
|
* this > dimColor. Crosses Box boundaries (Ink's style cascade doesn't). */
|
||||||
export const TextHoverColorContext = React.createContext<keyof Theme | undefined>(undefined);
|
export const TextHoverColorContext = React.createContext<
|
||||||
|
keyof Theme | undefined
|
||||||
|
>(undefined)
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
/**
|
/**
|
||||||
* Change text color. Accepts a theme key or raw color value.
|
* Change text color. Accepts a theme key or raw color value.
|
||||||
*/
|
*/
|
||||||
readonly color?: keyof Theme | Color;
|
readonly color?: keyof Theme | Color
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as `color`, but for background. Must be a theme key.
|
* Same as `color`, but for background. Must be a theme key.
|
||||||
*/
|
*/
|
||||||
readonly backgroundColor?: keyof Theme;
|
readonly backgroundColor?: keyof Theme
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dim the color using the theme's inactive color.
|
* Dim the color using the theme's inactive color.
|
||||||
* This is compatible with bold (unlike ANSI dim).
|
* This is compatible with bold (unlike ANSI dim).
|
||||||
*/
|
*/
|
||||||
readonly dimColor?: boolean;
|
readonly dimColor?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make the text bold.
|
* Make the text bold.
|
||||||
*/
|
*/
|
||||||
readonly bold?: boolean;
|
readonly bold?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make the text italic.
|
* Make the text italic.
|
||||||
*/
|
*/
|
||||||
readonly italic?: boolean;
|
readonly italic?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make the text underlined.
|
* Make the text underlined.
|
||||||
*/
|
*/
|
||||||
readonly underline?: boolean;
|
readonly underline?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make the text crossed with a line.
|
* Make the text crossed with a line.
|
||||||
*/
|
*/
|
||||||
readonly strikethrough?: boolean;
|
readonly strikethrough?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inverse background and foreground colors.
|
* Inverse background and foreground colors.
|
||||||
*/
|
*/
|
||||||
readonly inverse?: boolean;
|
readonly inverse?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This property tells Ink to wrap or truncate text if its width is larger than container.
|
* This property tells Ink to wrap or truncate text if its width is larger than container.
|
||||||
* If `wrap` is passed (by default), Ink will wrap text and split it into multiple lines.
|
* If `wrap` is passed (by default), Ink will wrap text and split it into multiple lines.
|
||||||
* If `truncate-*` is passed, Ink will truncate text instead, which will result in one line of text with the rest cut off.
|
* If `truncate-*` is passed, Ink will truncate text instead, which will result in one line of text with the rest cut off.
|
||||||
*/
|
*/
|
||||||
readonly wrap?: Styles['textWrap'];
|
readonly wrap?: Styles['textWrap']
|
||||||
readonly children?: ReactNode;
|
|
||||||
};
|
readonly children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a color value that may be a theme key to a raw Color.
|
* Resolves a color value that may be a theme key to a raw Color.
|
||||||
*/
|
*/
|
||||||
function resolveColor(color: keyof Theme | Color | undefined, theme: Theme): Color | undefined {
|
function resolveColor(
|
||||||
if (!color) return undefined;
|
color: keyof Theme | Color | undefined,
|
||||||
|
theme: Theme,
|
||||||
|
): Color | undefined {
|
||||||
|
if (!color) return undefined
|
||||||
// Check if it's a raw color (starts with rgb(, #, ansi256(, or ansi:)
|
// Check if it's a raw color (starts with rgb(, #, ansi256(, or ansi:)
|
||||||
if (color.startsWith('rgb(') || color.startsWith('#') || color.startsWith('ansi256(') || color.startsWith('ansi:')) {
|
if (
|
||||||
return color as Color;
|
color.startsWith('rgb(') ||
|
||||||
|
color.startsWith('#') ||
|
||||||
|
color.startsWith('ansi256(') ||
|
||||||
|
color.startsWith('ansi:')
|
||||||
|
) {
|
||||||
|
return color as Color
|
||||||
}
|
}
|
||||||
// It's a theme key - resolve it
|
// It's a theme key - resolve it
|
||||||
return theme[color as keyof Theme] as Color;
|
return theme[color as keyof Theme] as Color
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Theme-aware Text component that resolves theme color keys to raw colors.
|
* Theme-aware Text component that resolves theme color keys to raw colors.
|
||||||
* This wraps the base Text component with theme resolution.
|
* This wraps the base Text component with theme resolution.
|
||||||
*/
|
*/
|
||||||
export default function ThemedText(t0) {
|
export default function ThemedText({
|
||||||
const $ = _c(10);
|
color,
|
||||||
const {
|
backgroundColor,
|
||||||
color,
|
dimColor = false,
|
||||||
backgroundColor,
|
bold = false,
|
||||||
dimColor: t1,
|
italic = false,
|
||||||
bold: t2,
|
underline = false,
|
||||||
italic: t3,
|
strikethrough = false,
|
||||||
underline: t4,
|
inverse = false,
|
||||||
strikethrough: t5,
|
wrap = 'wrap',
|
||||||
inverse: t6,
|
children,
|
||||||
wrap: t7,
|
}: Props): React.ReactNode {
|
||||||
children
|
const [themeName] = useTheme()
|
||||||
} = t0;
|
const theme = getTheme(themeName)
|
||||||
const dimColor = t1 === undefined ? false : t1;
|
const hoverColor = useContext(TextHoverColorContext)
|
||||||
const bold = t2 === undefined ? false : t2;
|
|
||||||
const italic = t3 === undefined ? false : t3;
|
// Resolve theme keys to raw colors
|
||||||
const underline = t4 === undefined ? false : t4;
|
const resolvedColor =
|
||||||
const strikethrough = t5 === undefined ? false : t5;
|
!color && hoverColor
|
||||||
const inverse = t6 === undefined ? false : t6;
|
? resolveColor(hoverColor, theme)
|
||||||
const wrap = t7 === undefined ? "wrap" : t7;
|
: dimColor
|
||||||
const [themeName] = useTheme();
|
? (theme.inactive as Color)
|
||||||
const theme = getTheme(themeName);
|
: resolveColor(color, theme)
|
||||||
const hoverColor = useContext(TextHoverColorContext);
|
const resolvedBackgroundColor = backgroundColor
|
||||||
const resolvedColor = !color && hoverColor ? resolveColor(hoverColor, theme) : dimColor ? theme.inactive as Color : resolveColor(color, theme);
|
? (theme[backgroundColor] as Color)
|
||||||
const resolvedBackgroundColor = backgroundColor ? theme[backgroundColor] as Color : undefined;
|
: undefined
|
||||||
let t8;
|
|
||||||
if ($[0] !== bold || $[1] !== children || $[2] !== inverse || $[3] !== italic || $[4] !== resolvedBackgroundColor || $[5] !== resolvedColor || $[6] !== strikethrough || $[7] !== underline || $[8] !== wrap) {
|
return (
|
||||||
t8 = <Text color={resolvedColor} backgroundColor={resolvedBackgroundColor} bold={bold} italic={italic} underline={underline} strikethrough={strikethrough} inverse={inverse} wrap={wrap}>{children}</Text>;
|
<Text
|
||||||
$[0] = bold;
|
color={resolvedColor}
|
||||||
$[1] = children;
|
backgroundColor={resolvedBackgroundColor}
|
||||||
$[2] = inverse;
|
bold={bold}
|
||||||
$[3] = italic;
|
italic={italic}
|
||||||
$[4] = resolvedBackgroundColor;
|
underline={underline}
|
||||||
$[5] = resolvedColor;
|
strikethrough={strikethrough}
|
||||||
$[6] = strikethrough;
|
inverse={inverse}
|
||||||
$[7] = underline;
|
wrap={wrap}
|
||||||
$[8] = wrap;
|
>
|
||||||
$[9] = t8;
|
{children}
|
||||||
} else {
|
</Text>
|
||||||
t8 = $[9];
|
)
|
||||||
}
|
|
||||||
return t8;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,236 +1,205 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import capitalize from 'lodash-es/capitalize.js'
|
||||||
import capitalize from 'lodash-es/capitalize.js';
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { useMemo } from 'react'
|
||||||
import { useMemo } from 'react';
|
import {
|
||||||
import { type Command, type CommandBase, type CommandResultDisplay, getCommandName, type PromptCommand } from '../../commands.js';
|
type Command,
|
||||||
import { Box, Text } from '../../ink.js';
|
type CommandBase,
|
||||||
import { estimateSkillFrontmatterTokens, getSkillsPath } from '../../skills/loadSkillsDir.js';
|
type CommandResultDisplay,
|
||||||
import { getDisplayPath } from '../../utils/file.js';
|
getCommandName,
|
||||||
import { formatTokens } from '../../utils/format.js';
|
type PromptCommand,
|
||||||
import { getSettingSourceName, type SettingSource } from '../../utils/settings/constants.js';
|
} from '../../commands.js'
|
||||||
import { plural } from '../../utils/stringUtils.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
import {
|
||||||
import { Dialog } from '../design-system/Dialog.js';
|
estimateSkillFrontmatterTokens,
|
||||||
|
getSkillsPath,
|
||||||
|
} from '../../skills/loadSkillsDir.js'
|
||||||
|
import { getDisplayPath } from '../../utils/file.js'
|
||||||
|
import { formatTokens } from '../../utils/format.js'
|
||||||
|
import {
|
||||||
|
getSettingSourceName,
|
||||||
|
type SettingSource,
|
||||||
|
} from '../../utils/settings/constants.js'
|
||||||
|
import { plural } from '../../utils/stringUtils.js'
|
||||||
|
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||||
|
import { Dialog } from '../design-system/Dialog.js'
|
||||||
|
|
||||||
// Skills are always PromptCommands with CommandBase properties
|
// Skills are always PromptCommands with CommandBase properties
|
||||||
type SkillCommand = CommandBase & PromptCommand;
|
type SkillCommand = CommandBase & PromptCommand
|
||||||
type SkillSource = SettingSource | 'plugin' | 'mcp';
|
|
||||||
|
type SkillSource = SettingSource | 'plugin' | 'mcp'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onExit: (result?: string, options?: {
|
onExit: (
|
||||||
display?: CommandResultDisplay;
|
result?: string,
|
||||||
}) => void;
|
options?: { display?: CommandResultDisplay },
|
||||||
commands: Command[];
|
) => void
|
||||||
};
|
commands: Command[]
|
||||||
|
}
|
||||||
|
|
||||||
function getSourceTitle(source: SkillSource): string {
|
function getSourceTitle(source: SkillSource): string {
|
||||||
if (source === 'plugin') {
|
if (source === 'plugin') {
|
||||||
return 'Plugin skills';
|
return 'Plugin skills'
|
||||||
}
|
}
|
||||||
if (source === 'mcp') {
|
if (source === 'mcp') {
|
||||||
return 'MCP skills';
|
return 'MCP skills'
|
||||||
}
|
}
|
||||||
return `${capitalize(getSettingSourceName(source))} skills`;
|
return `${capitalize(getSettingSourceName(source))} skills`
|
||||||
}
|
}
|
||||||
function getSourceSubtitle(source: SkillSource, skills: SkillCommand[]): string | undefined {
|
|
||||||
|
function getSourceSubtitle(
|
||||||
|
source: SkillSource,
|
||||||
|
skills: SkillCommand[],
|
||||||
|
): string | undefined {
|
||||||
// MCP skills show server names; file-based skills show filesystem paths.
|
// MCP skills show server names; file-based skills show filesystem paths.
|
||||||
// Skill names are `<server>:<skill>`, not `mcp__<server>__…`.
|
// Skill names are `<server>:<skill>`, not `mcp__<server>__…`.
|
||||||
if (source === 'mcp') {
|
if (source === 'mcp') {
|
||||||
const servers = [...new Set(skills.map(s => {
|
const servers = [
|
||||||
const idx = s.name.indexOf(':');
|
...new Set(
|
||||||
return idx > 0 ? s.name.slice(0, idx) : null;
|
skills
|
||||||
}).filter((n): n is string => n != null))];
|
.map(s => {
|
||||||
return servers.length > 0 ? servers.join(', ') : undefined;
|
const idx = s.name.indexOf(':')
|
||||||
|
return idx > 0 ? s.name.slice(0, idx) : null
|
||||||
|
})
|
||||||
|
.filter((n): n is string => n != null),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
return servers.length > 0 ? servers.join(', ') : undefined
|
||||||
}
|
}
|
||||||
const skillsPath = getDisplayPath(getSkillsPath(source, 'skills'));
|
const skillsPath = getDisplayPath(getSkillsPath(source, 'skills'))
|
||||||
const hasCommandsSkills = skills.some(s => s.loadedFrom === 'commands_DEPRECATED');
|
const hasCommandsSkills = skills.some(
|
||||||
return hasCommandsSkills ? `${skillsPath}, ${getDisplayPath(getSkillsPath(source, 'commands'))}` : skillsPath;
|
s => s.loadedFrom === 'commands_DEPRECATED',
|
||||||
|
)
|
||||||
|
return hasCommandsSkills
|
||||||
|
? `${skillsPath}, ${getDisplayPath(getSkillsPath(source, 'commands'))}`
|
||||||
|
: skillsPath
|
||||||
}
|
}
|
||||||
export function SkillsMenu(t0) {
|
|
||||||
const $ = _c(35);
|
export function SkillsMenu({ onExit, commands }: Props): React.ReactNode {
|
||||||
const {
|
// Filter commands for skills and cast to SkillCommand
|
||||||
onExit,
|
const skills = useMemo(() => {
|
||||||
commands
|
return commands.filter(
|
||||||
} = t0;
|
(cmd): cmd is SkillCommand =>
|
||||||
let t1;
|
cmd.type === 'prompt' &&
|
||||||
if ($[0] !== commands) {
|
(cmd.loadedFrom === 'skills' ||
|
||||||
t1 = commands.filter(_temp);
|
cmd.loadedFrom === 'commands_DEPRECATED' ||
|
||||||
$[0] = commands;
|
cmd.loadedFrom === 'plugin' ||
|
||||||
$[1] = t1;
|
cmd.loadedFrom === 'mcp'),
|
||||||
} else {
|
)
|
||||||
t1 = $[1];
|
}, [commands])
|
||||||
}
|
|
||||||
const skills = t1;
|
const skillsBySource = useMemo((): Record<SkillSource, SkillCommand[]> => {
|
||||||
let groups;
|
const groups: Record<SkillSource, SkillCommand[]> = {
|
||||||
if ($[2] !== skills) {
|
|
||||||
groups = {
|
|
||||||
policySettings: [],
|
policySettings: [],
|
||||||
userSettings: [],
|
userSettings: [],
|
||||||
projectSettings: [],
|
projectSettings: [],
|
||||||
localSettings: [],
|
localSettings: [],
|
||||||
flagSettings: [],
|
flagSettings: [],
|
||||||
plugin: [],
|
plugin: [],
|
||||||
mcp: []
|
mcp: [],
|
||||||
};
|
}
|
||||||
|
|
||||||
for (const skill of skills) {
|
for (const skill of skills) {
|
||||||
const source = skill.source as SkillSource;
|
const source = skill.source as SkillSource
|
||||||
if (source in groups) {
|
if (source in groups) {
|
||||||
groups[source].push(skill);
|
groups[source].push(skill)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const group of Object.values(groups)) {
|
for (const group of Object.values(groups)) {
|
||||||
(group as Array<{ name: string }>).sort(_temp2);
|
group.sort((a, b) => getCommandName(a).localeCompare(getCommandName(b)))
|
||||||
}
|
}
|
||||||
$[2] = skills;
|
|
||||||
$[3] = groups;
|
return groups
|
||||||
} else {
|
}, [skills])
|
||||||
groups = $[3];
|
|
||||||
|
const handleCancel = (): void => {
|
||||||
|
onExit('Skills dialog dismissed', { display: 'system' })
|
||||||
}
|
}
|
||||||
const skillsBySource = groups;
|
|
||||||
let t2;
|
|
||||||
if ($[4] !== onExit) {
|
|
||||||
t2 = () => {
|
|
||||||
onExit("Skills dialog dismissed", {
|
|
||||||
display: "system"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
$[4] = onExit;
|
|
||||||
$[5] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[5];
|
|
||||||
}
|
|
||||||
const handleCancel = t2;
|
|
||||||
if (skills.length === 0) {
|
if (skills.length === 0) {
|
||||||
let t3;
|
return (
|
||||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
<Dialog
|
||||||
t3 = <Text dimColor={true}>Create skills in .claude/skills/ or ~/.claude/skills/</Text>;
|
title="Skills"
|
||||||
$[6] = t3;
|
subtitle="No skills found"
|
||||||
} else {
|
onCancel={handleCancel}
|
||||||
t3 = $[6];
|
hideInputGuide
|
||||||
}
|
>
|
||||||
let t4;
|
<Text dimColor>
|
||||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
Create skills in .claude/skills/ or ~/.claude/skills/
|
||||||
t4 = <Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text>;
|
</Text>
|
||||||
$[7] = t4;
|
<Text dimColor italic>
|
||||||
} else {
|
<ConfigurableShortcutHint
|
||||||
t4 = $[7];
|
action="confirm:no"
|
||||||
}
|
context="Confirmation"
|
||||||
let t5;
|
fallback="Esc"
|
||||||
if ($[8] !== handleCancel) {
|
description="close"
|
||||||
t5 = <Dialog title="Skills" subtitle="No skills found" onCancel={handleCancel} hideInputGuide={true}>{t3}{t4}</Dialog>;
|
/>
|
||||||
$[8] = handleCancel;
|
</Text>
|
||||||
$[9] = t5;
|
</Dialog>
|
||||||
} else {
|
)
|
||||||
t5 = $[9];
|
|
||||||
}
|
|
||||||
return t5;
|
|
||||||
}
|
}
|
||||||
const renderSkill = _temp3;
|
|
||||||
let t3;
|
const renderSkill = (skill: SkillCommand) => {
|
||||||
if ($[10] !== skillsBySource) {
|
const estimatedTokens = estimateSkillFrontmatterTokens(skill)
|
||||||
t3 = source_0 => {
|
const tokenDisplay = `~${formatTokens(estimatedTokens)}`
|
||||||
const groupSkills = skillsBySource[source_0];
|
const pluginName =
|
||||||
if (groupSkills.length === 0) {
|
skill.source === 'plugin'
|
||||||
return null;
|
? skill.pluginInfo?.pluginManifest.name
|
||||||
}
|
: undefined
|
||||||
const title = getSourceTitle(source_0);
|
|
||||||
const subtitle = getSourceSubtitle(source_0, groupSkills);
|
return (
|
||||||
return <Box flexDirection="column" key={source_0}><Box><Text bold={true} dimColor={true}>{title}</Text>{subtitle && <Text dimColor={true}> ({subtitle})</Text>}</Box>{groupSkills.map(skill_1 => renderSkill(skill_1))}</Box>;
|
<Box key={`${skill.name}-${skill.source}`}>
|
||||||
};
|
<Text>{getCommandName(skill)}</Text>
|
||||||
$[10] = skillsBySource;
|
<Text dimColor>
|
||||||
$[11] = t3;
|
{pluginName ? ` · ${pluginName}` : ''} · {tokenDisplay} description
|
||||||
} else {
|
tokens
|
||||||
t3 = $[11];
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const renderSkillGroup = t3;
|
|
||||||
const t4 = skills.length;
|
const renderSkillGroup = (source: SkillSource) => {
|
||||||
let t5;
|
const groupSkills = skillsBySource[source]
|
||||||
if ($[12] !== skills.length) {
|
if (groupSkills.length === 0) return null
|
||||||
t5 = plural(skills.length, "skill");
|
|
||||||
$[12] = skills.length;
|
const title = getSourceTitle(source)
|
||||||
$[13] = t5;
|
const subtitle = getSourceSubtitle(source, groupSkills)
|
||||||
} else {
|
|
||||||
t5 = $[13];
|
return (
|
||||||
|
<Box flexDirection="column" key={source}>
|
||||||
|
<Box>
|
||||||
|
<Text bold dimColor>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
{subtitle && <Text dimColor> ({subtitle})</Text>}
|
||||||
|
</Box>
|
||||||
|
{groupSkills.map(skill => renderSkill(skill))}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const t6 = `${t4} ${t5}`;
|
|
||||||
let t7;
|
return (
|
||||||
if ($[14] !== renderSkillGroup) {
|
<Dialog
|
||||||
t7 = renderSkillGroup("projectSettings");
|
title="Skills"
|
||||||
$[14] = renderSkillGroup;
|
subtitle={`${skills.length} ${plural(skills.length, 'skill')}`}
|
||||||
$[15] = t7;
|
onCancel={handleCancel}
|
||||||
} else {
|
hideInputGuide
|
||||||
t7 = $[15];
|
>
|
||||||
}
|
<Box flexDirection="column" gap={1}>
|
||||||
let t8;
|
{renderSkillGroup('projectSettings')}
|
||||||
if ($[16] !== renderSkillGroup) {
|
{renderSkillGroup('userSettings')}
|
||||||
t8 = renderSkillGroup("userSettings");
|
{renderSkillGroup('policySettings')}
|
||||||
$[16] = renderSkillGroup;
|
{renderSkillGroup('plugin')}
|
||||||
$[17] = t8;
|
{renderSkillGroup('mcp')}
|
||||||
} else {
|
</Box>
|
||||||
t8 = $[17];
|
<Text dimColor italic>
|
||||||
}
|
<ConfigurableShortcutHint
|
||||||
let t9;
|
action="confirm:no"
|
||||||
if ($[18] !== renderSkillGroup) {
|
context="Confirmation"
|
||||||
t9 = renderSkillGroup("policySettings");
|
fallback="Esc"
|
||||||
$[18] = renderSkillGroup;
|
description="close"
|
||||||
$[19] = t9;
|
/>
|
||||||
} else {
|
</Text>
|
||||||
t9 = $[19];
|
</Dialog>
|
||||||
}
|
)
|
||||||
let t10;
|
|
||||||
if ($[20] !== renderSkillGroup) {
|
|
||||||
t10 = renderSkillGroup("plugin");
|
|
||||||
$[20] = renderSkillGroup;
|
|
||||||
$[21] = t10;
|
|
||||||
} else {
|
|
||||||
t10 = $[21];
|
|
||||||
}
|
|
||||||
let t11;
|
|
||||||
if ($[22] !== renderSkillGroup) {
|
|
||||||
t11 = renderSkillGroup("mcp");
|
|
||||||
$[22] = renderSkillGroup;
|
|
||||||
$[23] = t11;
|
|
||||||
} else {
|
|
||||||
t11 = $[23];
|
|
||||||
}
|
|
||||||
let t12;
|
|
||||||
if ($[24] !== t10 || $[25] !== t11 || $[26] !== t7 || $[27] !== t8 || $[28] !== t9) {
|
|
||||||
t12 = <Box flexDirection="column" gap={1}>{t7}{t8}{t9}{t10}{t11}</Box>;
|
|
||||||
$[24] = t10;
|
|
||||||
$[25] = t11;
|
|
||||||
$[26] = t7;
|
|
||||||
$[27] = t8;
|
|
||||||
$[28] = t9;
|
|
||||||
$[29] = t12;
|
|
||||||
} else {
|
|
||||||
t12 = $[29];
|
|
||||||
}
|
|
||||||
let t13;
|
|
||||||
if ($[30] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t13 = <Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text>;
|
|
||||||
$[30] = t13;
|
|
||||||
} else {
|
|
||||||
t13 = $[30];
|
|
||||||
}
|
|
||||||
let t14;
|
|
||||||
if ($[31] !== handleCancel || $[32] !== t12 || $[33] !== t6) {
|
|
||||||
t14 = <Dialog title="Skills" subtitle={t6} onCancel={handleCancel} hideInputGuide={true}>{t12}{t13}</Dialog>;
|
|
||||||
$[31] = handleCancel;
|
|
||||||
$[32] = t12;
|
|
||||||
$[33] = t6;
|
|
||||||
$[34] = t14;
|
|
||||||
} else {
|
|
||||||
t14 = $[34];
|
|
||||||
}
|
|
||||||
return t14;
|
|
||||||
}
|
|
||||||
function _temp3(skill_0) {
|
|
||||||
const estimatedTokens = estimateSkillFrontmatterTokens(skill_0);
|
|
||||||
const tokenDisplay = `~${formatTokens(estimatedTokens)}`;
|
|
||||||
const pluginName = skill_0.source === "plugin" ? skill_0.pluginInfo?.pluginManifest.name : undefined;
|
|
||||||
return <Box key={`${skill_0.name}-${skill_0.source}`}><Text>{getCommandName(skill_0)}</Text><Text dimColor={true}>{pluginName ? ` · ${pluginName}` : ""} · {tokenDisplay} description tokens</Text></Box>;
|
|
||||||
}
|
|
||||||
function _temp2(a, b) {
|
|
||||||
return getCommandName(a).localeCompare(getCommandName(b));
|
|
||||||
}
|
|
||||||
function _temp(cmd) {
|
|
||||||
return cmd.type === "prompt" && (cmd.loadedFrom === "skills" || cmd.loadedFrom === "commands_DEPRECATED" || cmd.loadedFrom === "plugin" || cmd.loadedFrom === "mcp");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,228 +1,200 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { useMemo } from 'react'
|
||||||
import React, { useMemo } from 'react';
|
import type { DeepImmutable } from 'src/types/utils.js'
|
||||||
import type { DeepImmutable } from 'src/types/utils.js';
|
import { useElapsedTime } from '../../hooks/useElapsedTime.js'
|
||||||
import { useElapsedTime } from '../../hooks/useElapsedTime.js';
|
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
import { Box, Text, useTheme } from '../../ink.js'
|
||||||
import { Box, Text, useTheme } from '../../ink.js';
|
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
import { getEmptyToolPermissionContext } from '../../Tool.js'
|
||||||
import { getEmptyToolPermissionContext } from '../../Tool.js';
|
import type { LocalAgentTaskState } from '../../tasks/LocalAgentTask/LocalAgentTask.js'
|
||||||
import type { LocalAgentTaskState } from '../../tasks/LocalAgentTask/LocalAgentTask.js';
|
import { getTools } from '../../tools.js'
|
||||||
import { getTools } from '../../tools.js';
|
import { formatNumber } from '../../utils/format.js'
|
||||||
import { formatNumber } from '../../utils/format.js';
|
import { extractTag } from '../../utils/messages.js'
|
||||||
import { extractTag } from '../../utils/messages.js';
|
import { Byline } from '../design-system/Byline.js'
|
||||||
import { Byline } from '../design-system/Byline.js';
|
import { Dialog } from '../design-system/Dialog.js'
|
||||||
import { Dialog } from '../design-system/Dialog.js';
|
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
import { UserPlanMessage } from '../messages/UserPlanMessage.js'
|
||||||
import { UserPlanMessage } from '../messages/UserPlanMessage.js';
|
import { renderToolActivity } from './renderToolActivity.js'
|
||||||
import { renderToolActivity } from './renderToolActivity.js';
|
import { getTaskStatusColor, getTaskStatusIcon } from './taskStatusUtils.js'
|
||||||
import { getTaskStatusColor, getTaskStatusIcon } from './taskStatusUtils.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
agent: DeepImmutable<LocalAgentTaskState>;
|
agent: DeepImmutable<LocalAgentTaskState>
|
||||||
onDone: () => void;
|
onDone: () => void
|
||||||
onKillAgent?: () => void;
|
onKillAgent?: () => void
|
||||||
onBack?: () => void;
|
onBack?: () => void
|
||||||
};
|
}
|
||||||
export function AsyncAgentDetailDialog(t0) {
|
|
||||||
const $ = _c(54);
|
export function AsyncAgentDetailDialog({
|
||||||
const {
|
agent,
|
||||||
agent,
|
onDone,
|
||||||
onDone,
|
onKillAgent,
|
||||||
onKillAgent,
|
onBack,
|
||||||
onBack
|
}: Props): React.ReactNode {
|
||||||
} = t0;
|
const [theme] = useTheme()
|
||||||
const [theme] = useTheme();
|
|
||||||
let t1;
|
// Get tools for rendering activity messages
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
const tools = useMemo(() => getTools(getEmptyToolPermissionContext()), [])
|
||||||
t1 = getTools(getEmptyToolPermissionContext());
|
|
||||||
$[0] = t1;
|
const elapsedTime = useElapsedTime(
|
||||||
} else {
|
agent.startTime,
|
||||||
t1 = $[0];
|
agent.status === 'running',
|
||||||
}
|
1000,
|
||||||
const tools = t1;
|
agent.totalPausedMs ?? 0,
|
||||||
const elapsedTime = useElapsedTime(agent.startTime, agent.status === "running", 1000, agent.totalPausedMs ?? 0);
|
)
|
||||||
let t2;
|
|
||||||
if ($[1] !== onDone) {
|
// Restore confirm:yes (Enter/y) dismissal — Dialog handles confirm:no (Esc)
|
||||||
t2 = {
|
// internally but does NOT auto-wire confirm:yes.
|
||||||
"confirm:yes": onDone
|
useKeybindings(
|
||||||
};
|
{
|
||||||
$[1] = onDone;
|
'confirm:yes': onDone,
|
||||||
$[2] = t2;
|
},
|
||||||
} else {
|
{ context: 'Confirmation' },
|
||||||
t2 = $[2];
|
)
|
||||||
}
|
|
||||||
let t3;
|
// Component-specific shortcuts shown in UI hints (x=stop) and
|
||||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
// navigation keys (space=dismiss, left=back). These are context-dependent
|
||||||
t3 = {
|
// actions tied to agent state, not standard dialog keybindings.
|
||||||
context: "Confirmation"
|
// Note: Dialog component already handles ESC via confirm:no keybinding;
|
||||||
};
|
// confirm:yes (Enter/y) is handled by useKeybindings above.
|
||||||
$[3] = t3;
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
} else {
|
if (e.key === ' ') {
|
||||||
t3 = $[3];
|
e.preventDefault()
|
||||||
}
|
onDone()
|
||||||
useKeybindings(t2, t3);
|
} else if (e.key === 'left' && onBack) {
|
||||||
let t4;
|
e.preventDefault()
|
||||||
if ($[4] !== agent.status || $[5] !== onBack || $[6] !== onDone || $[7] !== onKillAgent) {
|
onBack()
|
||||||
t4 = e => {
|
} else if (e.key === 'x' && agent.status === 'running' && onKillAgent) {
|
||||||
if (e.key === " ") {
|
e.preventDefault()
|
||||||
e.preventDefault();
|
onKillAgent()
|
||||||
onDone();
|
}
|
||||||
} else {
|
}
|
||||||
if (e.key === "left" && onBack) {
|
|
||||||
e.preventDefault();
|
// Extract plan from prompt - if present, we show the plan instead of the prompt
|
||||||
onBack();
|
const planContent = extractTag(agent.prompt, 'plan')
|
||||||
} else {
|
|
||||||
if (e.key === "x" && agent.status === "running" && onKillAgent) {
|
const displayPrompt =
|
||||||
e.preventDefault();
|
agent.prompt.length > 300
|
||||||
onKillAgent();
|
? agent.prompt.substring(0, 297) + '…'
|
||||||
}
|
: agent.prompt
|
||||||
}
|
|
||||||
}
|
// Get tokens and tool uses (from result if completed, otherwise from progress)
|
||||||
};
|
const tokenCount = agent.result?.totalTokens ?? agent.progress?.tokenCount
|
||||||
$[4] = agent.status;
|
const toolUseCount =
|
||||||
$[5] = onBack;
|
agent.result?.totalToolUseCount ?? agent.progress?.toolUseCount
|
||||||
$[6] = onDone;
|
|
||||||
$[7] = onKillAgent;
|
const title = (
|
||||||
$[8] = t4;
|
<Text>
|
||||||
} else {
|
{agent.selectedAgent?.agentType ?? 'agent'} ›{' '}
|
||||||
t4 = $[8];
|
{agent.description || 'Async agent'}
|
||||||
}
|
</Text>
|
||||||
const handleKeyDown = t4;
|
)
|
||||||
let t5;
|
|
||||||
if ($[9] !== agent.prompt) {
|
// Build subtitle with status and stats
|
||||||
t5 = extractTag(agent.prompt, "plan");
|
const subtitle = (
|
||||||
$[9] = agent.prompt;
|
<Text>
|
||||||
$[10] = t5;
|
{agent.status !== 'running' && (
|
||||||
} else {
|
<Text color={getTaskStatusColor(agent.status)}>
|
||||||
t5 = $[10];
|
{getTaskStatusIcon(agent.status)}{' '}
|
||||||
}
|
{agent.status === 'completed'
|
||||||
const planContent = t5;
|
? 'Completed'
|
||||||
const displayPrompt = agent.prompt.length > 300 ? agent.prompt.substring(0, 297) + "\u2026" : agent.prompt;
|
: agent.status === 'failed'
|
||||||
const tokenCount = agent.result?.totalTokens ?? agent.progress?.tokenCount;
|
? 'Failed'
|
||||||
const toolUseCount = agent.result?.totalToolUseCount ?? agent.progress?.toolUseCount;
|
: 'Stopped'}
|
||||||
const t6 = agent.selectedAgent?.agentType ?? "agent";
|
{' · '}
|
||||||
const t7 = agent.description || "Async agent";
|
</Text>
|
||||||
let t8;
|
)}
|
||||||
if ($[11] !== t6 || $[12] !== t7) {
|
<Text dimColor>
|
||||||
t8 = <Text>{t6} ›{" "}{t7}</Text>;
|
{elapsedTime}
|
||||||
$[11] = t6;
|
{tokenCount !== undefined && tokenCount > 0 && (
|
||||||
$[12] = t7;
|
<> · {formatNumber(tokenCount)} tokens</>
|
||||||
$[13] = t8;
|
)}
|
||||||
} else {
|
{toolUseCount !== undefined && toolUseCount > 0 && (
|
||||||
t8 = $[13];
|
<>
|
||||||
}
|
{' '}
|
||||||
const title = t8;
|
· {toolUseCount} {toolUseCount === 1 ? 'tool' : 'tools'}
|
||||||
let t9;
|
</>
|
||||||
if ($[14] !== agent.status) {
|
)}
|
||||||
t9 = agent.status !== "running" && <Text color={getTaskStatusColor(agent.status)}>{getTaskStatusIcon(agent.status)}{" "}{agent.status === "completed" ? "Completed" : agent.status === "failed" ? "Failed" : "Stopped"}{" \xB7 "}</Text>;
|
</Text>
|
||||||
$[14] = agent.status;
|
</Text>
|
||||||
$[15] = t9;
|
)
|
||||||
} else {
|
|
||||||
t9 = $[15];
|
return (
|
||||||
}
|
<Box
|
||||||
let t10;
|
flexDirection="column"
|
||||||
if ($[16] !== tokenCount) {
|
tabIndex={0}
|
||||||
t10 = tokenCount !== undefined && tokenCount > 0 && <> · {formatNumber(tokenCount)} tokens</>;
|
autoFocus
|
||||||
$[16] = tokenCount;
|
onKeyDown={handleKeyDown}
|
||||||
$[17] = t10;
|
>
|
||||||
} else {
|
<Dialog
|
||||||
t10 = $[17];
|
title={title}
|
||||||
}
|
subtitle={subtitle}
|
||||||
let t11;
|
onCancel={onDone}
|
||||||
if ($[18] !== toolUseCount) {
|
color="background"
|
||||||
t11 = toolUseCount !== undefined && toolUseCount > 0 && <>{" "}· {toolUseCount} {toolUseCount === 1 ? "tool" : "tools"}</>;
|
inputGuide={exitState =>
|
||||||
$[18] = toolUseCount;
|
exitState.pending ? (
|
||||||
$[19] = t11;
|
<Text>Press {exitState.keyName} again to exit</Text>
|
||||||
} else {
|
) : (
|
||||||
t11 = $[19];
|
<Byline>
|
||||||
}
|
{onBack && <KeyboardShortcutHint shortcut="←" action="go back" />}
|
||||||
let t12;
|
<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />
|
||||||
if ($[20] !== elapsedTime || $[21] !== t10 || $[22] !== t11) {
|
{agent.status === 'running' && onKillAgent && (
|
||||||
t12 = <Text dimColor={true}>{elapsedTime}{t10}{t11}</Text>;
|
<KeyboardShortcutHint shortcut="x" action="stop" />
|
||||||
$[20] = elapsedTime;
|
)}
|
||||||
$[21] = t10;
|
</Byline>
|
||||||
$[22] = t11;
|
)
|
||||||
$[23] = t12;
|
}
|
||||||
} else {
|
>
|
||||||
t12 = $[23];
|
<Box flexDirection="column">
|
||||||
}
|
{/* Recent activities for running agents */}
|
||||||
let t13;
|
{agent.status === 'running' &&
|
||||||
if ($[24] !== t12 || $[25] !== t9) {
|
agent.progress?.recentActivities &&
|
||||||
t13 = <Text>{t9}{t12}</Text>;
|
agent.progress.recentActivities.length > 0 && (
|
||||||
$[24] = t12;
|
<Box flexDirection="column">
|
||||||
$[25] = t9;
|
<Text bold dimColor>
|
||||||
$[26] = t13;
|
Progress
|
||||||
} else {
|
</Text>
|
||||||
t13 = $[26];
|
{agent.progress.recentActivities.map((activity, i) => (
|
||||||
}
|
<Text
|
||||||
const subtitle = t13;
|
key={i}
|
||||||
let t14;
|
dimColor={i < agent.progress!.recentActivities!.length - 1}
|
||||||
if ($[27] !== agent.status || $[28] !== onBack || $[29] !== onKillAgent) {
|
wrap="truncate-end"
|
||||||
t14 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{agent.status === "running" && onKillAgent && <KeyboardShortcutHint shortcut="x" action="stop" />}</Byline>;
|
>
|
||||||
$[27] = agent.status;
|
{i === agent.progress!.recentActivities!.length - 1
|
||||||
$[28] = onBack;
|
? '› '
|
||||||
$[29] = onKillAgent;
|
: ' '}
|
||||||
$[30] = t14;
|
{renderToolActivity(activity, tools, theme)}
|
||||||
} else {
|
</Text>
|
||||||
t14 = $[30];
|
))}
|
||||||
}
|
</Box>
|
||||||
let t15;
|
)}
|
||||||
if ($[31] !== agent.progress || $[32] !== agent.status || $[33] !== theme) {
|
|
||||||
t15 = agent.status === "running" && agent.progress?.recentActivities && agent.progress.recentActivities.length > 0 && <Box flexDirection="column"><Text bold={true} dimColor={true}>Progress</Text>{agent.progress.recentActivities.map((activity, i) => <Text key={i} dimColor={i < agent.progress.recentActivities.length - 1} wrap="truncate-end">{i === agent.progress.recentActivities.length - 1 ? "\u203A " : " "}{renderToolActivity(activity, tools, theme)}</Text>)}</Box>;
|
{/* Plan section (if present) - shown instead of prompt */}
|
||||||
$[31] = agent.progress;
|
{planContent ? (
|
||||||
$[32] = agent.status;
|
<Box marginTop={1}>
|
||||||
$[33] = theme;
|
<UserPlanMessage addMargin={false} planContent={planContent} />
|
||||||
$[34] = t15;
|
</Box>
|
||||||
} else {
|
) : (
|
||||||
t15 = $[34];
|
/* Prompt section - only shown when no plan */
|
||||||
}
|
<Box flexDirection="column" marginTop={1}>
|
||||||
let t16;
|
<Text bold dimColor>
|
||||||
if ($[35] !== displayPrompt || $[36] !== planContent) {
|
Prompt
|
||||||
t16 = planContent ? <Box marginTop={1}><UserPlanMessage addMargin={false} planContent={planContent} /></Box> : <Box flexDirection="column" marginTop={1}><Text bold={true} dimColor={true}>Prompt</Text><Text wrap="wrap">{displayPrompt}</Text></Box>;
|
</Text>
|
||||||
$[35] = displayPrompt;
|
<Text wrap="wrap">{displayPrompt}</Text>
|
||||||
$[36] = planContent;
|
</Box>
|
||||||
$[37] = t16;
|
)}
|
||||||
} else {
|
|
||||||
t16 = $[37];
|
{/* Error details if failed */}
|
||||||
}
|
{agent.status === 'failed' && agent.error && (
|
||||||
let t17;
|
<Box flexDirection="column" marginTop={1}>
|
||||||
if ($[38] !== agent.error || $[39] !== agent.status) {
|
<Text bold color="error">
|
||||||
t17 = agent.status === "failed" && agent.error && <Box flexDirection="column" marginTop={1}><Text bold={true} color="error">Error</Text><Text color="error" wrap="wrap">{agent.error}</Text></Box>;
|
Error
|
||||||
$[38] = agent.error;
|
</Text>
|
||||||
$[39] = agent.status;
|
<Text color="error" wrap="wrap">
|
||||||
$[40] = t17;
|
{agent.error}
|
||||||
} else {
|
</Text>
|
||||||
t17 = $[40];
|
</Box>
|
||||||
}
|
)}
|
||||||
let t18;
|
</Box>
|
||||||
if ($[41] !== t15 || $[42] !== t16 || $[43] !== t17) {
|
</Dialog>
|
||||||
t18 = <Box flexDirection="column">{t15}{t16}{t17}</Box>;
|
</Box>
|
||||||
$[41] = t15;
|
)
|
||||||
$[42] = t16;
|
|
||||||
$[43] = t17;
|
|
||||||
$[44] = t18;
|
|
||||||
} else {
|
|
||||||
t18 = $[44];
|
|
||||||
}
|
|
||||||
let t19;
|
|
||||||
if ($[45] !== onDone || $[46] !== subtitle || $[47] !== t14 || $[48] !== t18 || $[49] !== title) {
|
|
||||||
t19 = <Dialog title={title} subtitle={subtitle} onCancel={onDone} color="background" inputGuide={t14}>{t18}</Dialog>;
|
|
||||||
$[45] = onDone;
|
|
||||||
$[46] = subtitle;
|
|
||||||
$[47] = t14;
|
|
||||||
$[48] = t18;
|
|
||||||
$[49] = title;
|
|
||||||
$[50] = t19;
|
|
||||||
} else {
|
|
||||||
t19 = $[50];
|
|
||||||
}
|
|
||||||
let t20;
|
|
||||||
if ($[51] !== handleKeyDown || $[52] !== t19) {
|
|
||||||
t20 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t19}</Box>;
|
|
||||||
$[51] = handleKeyDown;
|
|
||||||
$[52] = t19;
|
|
||||||
$[53] = t20;
|
|
||||||
} else {
|
|
||||||
t20 = $[53];
|
|
||||||
}
|
|
||||||
return t20;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,344 +1,146 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { Text } from 'src/ink.js'
|
||||||
import { Text } from 'src/ink.js';
|
import type { BackgroundTaskState } from 'src/tasks/types.js'
|
||||||
import type { BackgroundTaskState } from 'src/tasks/types.js';
|
import type { DeepImmutable } from 'src/types/utils.js'
|
||||||
import type { DeepImmutable } from 'src/types/utils.js';
|
import { truncate } from 'src/utils/format.js'
|
||||||
import { truncate } from 'src/utils/format.js';
|
import { toInkColor } from 'src/utils/ink.js'
|
||||||
import { toInkColor } from 'src/utils/ink.js';
|
import { plural } from 'src/utils/stringUtils.js'
|
||||||
import { plural } from 'src/utils/stringUtils.js';
|
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'
|
||||||
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js';
|
import { RemoteSessionProgress } from './RemoteSessionProgress.js'
|
||||||
import { RemoteSessionProgress } from './RemoteSessionProgress.js';
|
import { ShellProgress, TaskStatusText } from './ShellProgress.js'
|
||||||
import { ShellProgress, TaskStatusText } from './ShellProgress.js';
|
import { describeTeammateActivity } from './taskStatusUtils.js'
|
||||||
import { describeTeammateActivity } from './taskStatusUtils.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
task: DeepImmutable<BackgroundTaskState>;
|
task: DeepImmutable<BackgroundTaskState>
|
||||||
maxActivityWidth?: number;
|
maxActivityWidth?: number
|
||||||
};
|
}
|
||||||
export function BackgroundTask(t0) {
|
|
||||||
const $ = _c(92);
|
export function BackgroundTask({
|
||||||
const {
|
task,
|
||||||
task,
|
maxActivityWidth,
|
||||||
maxActivityWidth
|
}: Props): React.ReactNode {
|
||||||
} = t0;
|
const activityLimit = maxActivityWidth ?? 40
|
||||||
const activityLimit = maxActivityWidth ?? 40;
|
|
||||||
switch (task.type) {
|
switch (task.type) {
|
||||||
case "local_bash":
|
case 'local_bash':
|
||||||
{
|
return (
|
||||||
const t1 = task.kind === "monitor" ? task.description : task.command;
|
<Text>
|
||||||
let t2;
|
{truncate(
|
||||||
if ($[0] !== activityLimit || $[1] !== t1) {
|
task.kind === 'monitor' ? task.description : task.command,
|
||||||
t2 = truncate(t1, activityLimit, true);
|
activityLimit,
|
||||||
$[0] = activityLimit;
|
true,
|
||||||
$[1] = t1;
|
)}{' '}
|
||||||
$[2] = t2;
|
<ShellProgress shell={task} />
|
||||||
} else {
|
</Text>
|
||||||
t2 = $[2];
|
)
|
||||||
}
|
case 'remote_agent': {
|
||||||
let t3;
|
// Lite-review renders its own rainbow line (title + live counts),
|
||||||
if ($[3] !== task) {
|
// so we don't prefix the title — the rainbow already includes it.
|
||||||
t3 = <ShellProgress shell={task} />;
|
if (task.isRemoteReview) {
|
||||||
$[3] = task;
|
return (
|
||||||
$[4] = t3;
|
<Text>
|
||||||
} else {
|
<RemoteSessionProgress session={task} />
|
||||||
t3 = $[4];
|
</Text>
|
||||||
}
|
)
|
||||||
let t4;
|
|
||||||
if ($[5] !== t2 || $[6] !== t3) {
|
|
||||||
t4 = <Text>{t2}{" "}{t3}</Text>;
|
|
||||||
$[5] = t2;
|
|
||||||
$[6] = t3;
|
|
||||||
$[7] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[7];
|
|
||||||
}
|
|
||||||
return t4;
|
|
||||||
}
|
|
||||||
case "remote_agent":
|
|
||||||
{
|
|
||||||
if (task.isRemoteReview) {
|
|
||||||
let t1;
|
|
||||||
if ($[8] !== task) {
|
|
||||||
t1 = <Text><RemoteSessionProgress session={task} /></Text>;
|
|
||||||
$[8] = task;
|
|
||||||
$[9] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[9];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
|
||||||
const running = task.status === "running" || task.status === "pending";
|
|
||||||
const t1 = running ? DIAMOND_OPEN : DIAMOND_FILLED;
|
|
||||||
let t2;
|
|
||||||
if ($[10] !== t1) {
|
|
||||||
t2 = <Text dimColor={true}>{t1} </Text>;
|
|
||||||
$[10] = t1;
|
|
||||||
$[11] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[11];
|
|
||||||
}
|
|
||||||
let t3;
|
|
||||||
if ($[12] !== activityLimit || $[13] !== task.title) {
|
|
||||||
t3 = truncate(task.title, activityLimit, true);
|
|
||||||
$[12] = activityLimit;
|
|
||||||
$[13] = task.title;
|
|
||||||
$[14] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[14];
|
|
||||||
}
|
|
||||||
let t4;
|
|
||||||
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t4 = <Text dimColor={true}> · </Text>;
|
|
||||||
$[15] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[15];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[16] !== task) {
|
|
||||||
t5 = <RemoteSessionProgress session={task} />;
|
|
||||||
$[16] = task;
|
|
||||||
$[17] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[17];
|
|
||||||
}
|
|
||||||
let t6;
|
|
||||||
if ($[18] !== t2 || $[19] !== t3 || $[20] !== t5) {
|
|
||||||
t6 = <Text>{t2}{t3}{t4}{t5}</Text>;
|
|
||||||
$[18] = t2;
|
|
||||||
$[19] = t3;
|
|
||||||
$[20] = t5;
|
|
||||||
$[21] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[21];
|
|
||||||
}
|
|
||||||
return t6;
|
|
||||||
}
|
|
||||||
case "local_agent":
|
|
||||||
{
|
|
||||||
let t1;
|
|
||||||
if ($[22] !== activityLimit || $[23] !== task.description) {
|
|
||||||
t1 = truncate(task.description, activityLimit, true);
|
|
||||||
$[22] = activityLimit;
|
|
||||||
$[23] = task.description;
|
|
||||||
$[24] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[24];
|
|
||||||
}
|
|
||||||
const t2 = task.status === "completed" ? "done" : undefined;
|
|
||||||
const t3 = task.status === "completed" && !task.notified ? ", unread" : undefined;
|
|
||||||
let t4;
|
|
||||||
if ($[25] !== t2 || $[26] !== t3 || $[27] !== task.status) {
|
|
||||||
t4 = <TaskStatusText status={task.status} label={t2} suffix={t3} />;
|
|
||||||
$[25] = t2;
|
|
||||||
$[26] = t3;
|
|
||||||
$[27] = task.status;
|
|
||||||
$[28] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[28];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[29] !== t1 || $[30] !== t4) {
|
|
||||||
t5 = <Text>{t1}{" "}{t4}</Text>;
|
|
||||||
$[29] = t1;
|
|
||||||
$[30] = t4;
|
|
||||||
$[31] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[31];
|
|
||||||
}
|
|
||||||
return t5;
|
|
||||||
}
|
|
||||||
case "in_process_teammate":
|
|
||||||
{
|
|
||||||
let T0;
|
|
||||||
let T1;
|
|
||||||
let t1;
|
|
||||||
let t2;
|
|
||||||
let t3;
|
|
||||||
let t4;
|
|
||||||
if ($[32] !== activityLimit || $[33] !== task) {
|
|
||||||
const activity = describeTeammateActivity(task);
|
|
||||||
T1 = Text;
|
|
||||||
let t5;
|
|
||||||
if ($[40] !== task.identity.color) {
|
|
||||||
t5 = toInkColor(task.identity.color);
|
|
||||||
$[40] = task.identity.color;
|
|
||||||
$[41] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[41];
|
|
||||||
}
|
|
||||||
if ($[42] !== t5 || $[43] !== task.identity.agentName) {
|
|
||||||
t4 = <Text color={t5}>@{task.identity.agentName}</Text>;
|
|
||||||
$[42] = t5;
|
|
||||||
$[43] = task.identity.agentName;
|
|
||||||
$[44] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[44];
|
|
||||||
}
|
|
||||||
T0 = Text;
|
|
||||||
t1 = true;
|
|
||||||
t2 = ": ";
|
|
||||||
t3 = truncate(activity, activityLimit, true);
|
|
||||||
$[32] = activityLimit;
|
|
||||||
$[33] = task;
|
|
||||||
$[34] = T0;
|
|
||||||
$[35] = T1;
|
|
||||||
$[36] = t1;
|
|
||||||
$[37] = t2;
|
|
||||||
$[38] = t3;
|
|
||||||
$[39] = t4;
|
|
||||||
} else {
|
|
||||||
T0 = $[34];
|
|
||||||
T1 = $[35];
|
|
||||||
t1 = $[36];
|
|
||||||
t2 = $[37];
|
|
||||||
t3 = $[38];
|
|
||||||
t4 = $[39];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[45] !== T0 || $[46] !== t1 || $[47] !== t2 || $[48] !== t3) {
|
|
||||||
t5 = <T0 dimColor={t1}>{t2}{t3}</T0>;
|
|
||||||
$[45] = T0;
|
|
||||||
$[46] = t1;
|
|
||||||
$[47] = t2;
|
|
||||||
$[48] = t3;
|
|
||||||
$[49] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[49];
|
|
||||||
}
|
|
||||||
let t6;
|
|
||||||
if ($[50] !== T1 || $[51] !== t4 || $[52] !== t5) {
|
|
||||||
t6 = <T1>{t4}{t5}</T1>;
|
|
||||||
$[50] = T1;
|
|
||||||
$[51] = t4;
|
|
||||||
$[52] = t5;
|
|
||||||
$[53] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[53];
|
|
||||||
}
|
|
||||||
return t6;
|
|
||||||
}
|
|
||||||
case "local_workflow":
|
|
||||||
{
|
|
||||||
const t1 = task.workflowName ?? task.summary ?? task.description;
|
|
||||||
let t2;
|
|
||||||
if ($[54] !== activityLimit || $[55] !== t1) {
|
|
||||||
t2 = truncate(t1, activityLimit, true);
|
|
||||||
$[54] = activityLimit;
|
|
||||||
$[55] = t1;
|
|
||||||
$[56] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[56];
|
|
||||||
}
|
|
||||||
let t3;
|
|
||||||
if ($[57] !== task.agentCount || $[58] !== task.status) {
|
|
||||||
t3 = task.status === "running" ? `${task.agentCount} ${plural(task.agentCount, "agent")}` : task.status === "completed" ? "done" : undefined;
|
|
||||||
$[57] = task.agentCount;
|
|
||||||
$[58] = task.status;
|
|
||||||
$[59] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[59];
|
|
||||||
}
|
|
||||||
const t4 = task.status === "completed" && !task.notified ? ", unread" : undefined;
|
|
||||||
let t5;
|
|
||||||
if ($[60] !== t3 || $[61] !== t4 || $[62] !== task.status) {
|
|
||||||
t5 = <TaskStatusText status={task.status} label={t3} suffix={t4} />;
|
|
||||||
$[60] = t3;
|
|
||||||
$[61] = t4;
|
|
||||||
$[62] = task.status;
|
|
||||||
$[63] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[63];
|
|
||||||
}
|
|
||||||
let t6;
|
|
||||||
if ($[64] !== t2 || $[65] !== t5) {
|
|
||||||
t6 = <Text>{t2}{" "}{t5}</Text>;
|
|
||||||
$[64] = t2;
|
|
||||||
$[65] = t5;
|
|
||||||
$[66] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[66];
|
|
||||||
}
|
|
||||||
return t6;
|
|
||||||
}
|
|
||||||
case "monitor_mcp":
|
|
||||||
{
|
|
||||||
let t1;
|
|
||||||
if ($[67] !== activityLimit || $[68] !== task.description) {
|
|
||||||
t1 = truncate(task.description, activityLimit, true);
|
|
||||||
$[67] = activityLimit;
|
|
||||||
$[68] = task.description;
|
|
||||||
$[69] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[69];
|
|
||||||
}
|
|
||||||
const t2 = task.status === "completed" ? "done" : undefined;
|
|
||||||
const t3 = task.status === "completed" && !task.notified ? ", unread" : undefined;
|
|
||||||
let t4;
|
|
||||||
if ($[70] !== t2 || $[71] !== t3 || $[72] !== task.status) {
|
|
||||||
t4 = <TaskStatusText status={task.status} label={t2} suffix={t3} />;
|
|
||||||
$[70] = t2;
|
|
||||||
$[71] = t3;
|
|
||||||
$[72] = task.status;
|
|
||||||
$[73] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[73];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[74] !== t1 || $[75] !== t4) {
|
|
||||||
t5 = <Text>{t1}{" "}{t4}</Text>;
|
|
||||||
$[74] = t1;
|
|
||||||
$[75] = t4;
|
|
||||||
$[76] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[76];
|
|
||||||
}
|
|
||||||
return t5;
|
|
||||||
}
|
|
||||||
case "dream":
|
|
||||||
{
|
|
||||||
const n = task.filesTouched.length;
|
|
||||||
let t1;
|
|
||||||
if ($[77] !== n || $[78] !== task.phase || $[79] !== task.sessionsReviewing) {
|
|
||||||
t1 = task.phase === "updating" && n > 0 ? `${n} ${plural(n, "file")}` : `${task.sessionsReviewing} ${plural(task.sessionsReviewing, "session")}`;
|
|
||||||
$[77] = n;
|
|
||||||
$[78] = task.phase;
|
|
||||||
$[79] = task.sessionsReviewing;
|
|
||||||
$[80] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[80];
|
|
||||||
}
|
|
||||||
const detail = t1;
|
|
||||||
let t2;
|
|
||||||
if ($[81] !== detail || $[82] !== task.phase) {
|
|
||||||
t2 = <Text dimColor={true}>· {task.phase} · {detail}</Text>;
|
|
||||||
$[81] = detail;
|
|
||||||
$[82] = task.phase;
|
|
||||||
$[83] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[83];
|
|
||||||
}
|
|
||||||
const t3 = task.status === "completed" ? "done" : undefined;
|
|
||||||
const t4 = task.status === "completed" && !task.notified ? ", unread" : undefined;
|
|
||||||
let t5;
|
|
||||||
if ($[84] !== t3 || $[85] !== t4 || $[86] !== task.status) {
|
|
||||||
t5 = <TaskStatusText status={task.status} label={t3} suffix={t4} />;
|
|
||||||
$[84] = t3;
|
|
||||||
$[85] = t4;
|
|
||||||
$[86] = task.status;
|
|
||||||
$[87] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[87];
|
|
||||||
}
|
|
||||||
let t6;
|
|
||||||
if ($[88] !== t2 || $[89] !== t5 || $[90] !== task.description) {
|
|
||||||
t6 = <Text>{task.description}{" "}{t2}{" "}{t5}</Text>;
|
|
||||||
$[88] = t2;
|
|
||||||
$[89] = t5;
|
|
||||||
$[90] = task.description;
|
|
||||||
$[91] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[91];
|
|
||||||
}
|
|
||||||
return t6;
|
|
||||||
}
|
}
|
||||||
|
const running = task.status === 'running' || task.status === 'pending'
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
<Text dimColor>{running ? DIAMOND_OPEN : DIAMOND_FILLED} </Text>
|
||||||
|
{truncate(task.title, activityLimit, true)}
|
||||||
|
<Text dimColor> · </Text>
|
||||||
|
<RemoteSessionProgress session={task} />
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case 'local_agent':
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
{truncate(task.description, activityLimit, true)}{' '}
|
||||||
|
<TaskStatusText
|
||||||
|
status={task.status}
|
||||||
|
label={task.status === 'completed' ? 'done' : undefined}
|
||||||
|
suffix={
|
||||||
|
task.status === 'completed' && !task.notified
|
||||||
|
? ', unread'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
case 'in_process_teammate': {
|
||||||
|
const activity = describeTeammateActivity(task)
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
<Text color={toInkColor(task.identity.color)}>
|
||||||
|
@{task.identity.agentName}
|
||||||
|
</Text>
|
||||||
|
<Text dimColor>: {truncate(activity, activityLimit, true)}</Text>
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case 'local_workflow':
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
{truncate(
|
||||||
|
task.workflowName ?? task.summary ?? task.description,
|
||||||
|
activityLimit,
|
||||||
|
true,
|
||||||
|
)}{' '}
|
||||||
|
<TaskStatusText
|
||||||
|
status={task.status}
|
||||||
|
label={
|
||||||
|
task.status === 'running'
|
||||||
|
? `${task.agentCount} ${plural(task.agentCount, 'agent')}`
|
||||||
|
: task.status === 'completed'
|
||||||
|
? 'done'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
suffix={
|
||||||
|
task.status === 'completed' && !task.notified
|
||||||
|
? ', unread'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
case 'monitor_mcp':
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
{truncate(task.description, activityLimit, true)}{' '}
|
||||||
|
<TaskStatusText
|
||||||
|
status={task.status}
|
||||||
|
label={task.status === 'completed' ? 'done' : undefined}
|
||||||
|
suffix={
|
||||||
|
task.status === 'completed' && !task.notified
|
||||||
|
? ', unread'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
case 'dream': {
|
||||||
|
const n = task.filesTouched.length
|
||||||
|
const detail =
|
||||||
|
task.phase === 'updating' && n > 0
|
||||||
|
? `${n} ${plural(n, 'file')}`
|
||||||
|
: `${task.sessionsReviewing} ${plural(task.sessionsReviewing, 'session')}`
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
{task.description}{' '}
|
||||||
|
<Text dimColor>
|
||||||
|
· {task.phase} · {detail}
|
||||||
|
</Text>{' '}
|
||||||
|
<TaskStatusText
|
||||||
|
status={task.status}
|
||||||
|
label={task.status === 'completed' ? 'done' : undefined}
|
||||||
|
suffix={
|
||||||
|
task.status === 'completed' && !task.notified
|
||||||
|
? ', unread'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,428 +1,310 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import figures from 'figures'
|
||||||
import figures from 'figures';
|
import * as React from 'react'
|
||||||
import * as React from 'react';
|
import { useMemo, useState } from 'react'
|
||||||
import { useMemo, useState } from 'react';
|
import { useTerminalSize } from 'src/hooks/useTerminalSize.js'
|
||||||
import { useTerminalSize } from 'src/hooks/useTerminalSize.js';
|
import { stringWidth } from 'src/ink/stringWidth.js'
|
||||||
import { stringWidth } from 'src/ink/stringWidth.js';
|
import { useAppState, useSetAppState } from 'src/state/AppState.js'
|
||||||
import { useAppState, useSetAppState } from 'src/state/AppState.js';
|
import {
|
||||||
import { enterTeammateView, exitTeammateView } from 'src/state/teammateViewHelpers.js';
|
enterTeammateView,
|
||||||
import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js';
|
exitTeammateView,
|
||||||
import { getPillLabel, pillNeedsCta } from 'src/tasks/pillLabel.js';
|
} from 'src/state/teammateViewHelpers.js'
|
||||||
import { type BackgroundTaskState, isBackgroundTask, type TaskState } from 'src/tasks/types.js';
|
import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
|
||||||
import { calculateHorizontalScrollWindow } from 'src/utils/horizontalScroll.js';
|
import { getPillLabel, pillNeedsCta } from 'src/tasks/pillLabel.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import {
|
||||||
import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from '../../tools/AgentTool/agentColorManager.js';
|
type BackgroundTaskState,
|
||||||
import type { Theme } from '../../utils/theme.js';
|
isBackgroundTask,
|
||||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
type TaskState,
|
||||||
import { shouldHideTasksFooter } from './taskStatusUtils.js';
|
} from 'src/tasks/types.js'
|
||||||
|
import { calculateHorizontalScrollWindow } from 'src/utils/horizontalScroll.js'
|
||||||
|
import { Box, Text } from '../../ink.js'
|
||||||
|
import {
|
||||||
|
AGENT_COLOR_TO_THEME_COLOR,
|
||||||
|
AGENT_COLORS,
|
||||||
|
type AgentColorName,
|
||||||
|
} from '../../tools/AgentTool/agentColorManager.js'
|
||||||
|
import type { Theme } from '../../utils/theme.js'
|
||||||
|
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||||
|
import { shouldHideTasksFooter } from './taskStatusUtils.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tasksSelected: boolean;
|
tasksSelected: boolean
|
||||||
isViewingTeammate?: boolean;
|
isViewingTeammate?: boolean
|
||||||
teammateFooterIndex?: number;
|
teammateFooterIndex?: number
|
||||||
isLeaderIdle?: boolean;
|
isLeaderIdle?: boolean
|
||||||
onOpenDialog?: (taskId?: string) => void;
|
onOpenDialog?: (taskId?: string) => void
|
||||||
};
|
}
|
||||||
export function BackgroundTaskStatus(t0) {
|
|
||||||
const $ = _c(48);
|
export function BackgroundTaskStatus({
|
||||||
const {
|
tasksSelected,
|
||||||
tasksSelected,
|
isViewingTeammate,
|
||||||
isViewingTeammate,
|
teammateFooterIndex = 0,
|
||||||
teammateFooterIndex: t1,
|
isLeaderIdle = false,
|
||||||
isLeaderIdle: t2,
|
onOpenDialog,
|
||||||
onOpenDialog
|
}: Props): React.ReactNode {
|
||||||
} = t0;
|
const setAppState = useSetAppState()
|
||||||
const teammateFooterIndex = t1 === undefined ? 0 : t1;
|
const { columns } = useTerminalSize()
|
||||||
const isLeaderIdle = t2 === undefined ? false : t2;
|
const tasks = useAppState(s => s.tasks)
|
||||||
const setAppState = useSetAppState();
|
const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId)
|
||||||
const {
|
|
||||||
columns
|
const runningTasks = useMemo(
|
||||||
} = useTerminalSize();
|
() =>
|
||||||
const tasks = useAppState(_temp);
|
(Object.values(tasks ?? {}) as TaskState[]).filter(
|
||||||
const viewingAgentTaskId = useAppState(_temp2);
|
t =>
|
||||||
let t3;
|
isBackgroundTask(t) &&
|
||||||
if ($[0] !== tasks) {
|
!(process.env.USER_TYPE === 'ant' && isPanelAgentTask(t)),
|
||||||
t3 = (Object.values(tasks ?? {}) as TaskState[]).filter(_temp3);
|
),
|
||||||
$[0] = tasks;
|
[tasks],
|
||||||
$[1] = t3;
|
)
|
||||||
} else {
|
|
||||||
t3 = $[1];
|
// Check if all tasks are in-process teammates (team mode)
|
||||||
}
|
// In spinner-tree mode, don't show teammate pills (teammates appear in the spinner tree)
|
||||||
const runningTasks = t3;
|
const expandedView = useAppState(s => s.expandedView)
|
||||||
const expandedView = useAppState(_temp4);
|
const showSpinnerTree = expandedView === 'teammates'
|
||||||
const showSpinnerTree = expandedView === "teammates";
|
const allTeammates =
|
||||||
const allTeammates = !showSpinnerTree && runningTasks.length > 0 && runningTasks.every(_temp5);
|
!showSpinnerTree &&
|
||||||
let t4;
|
runningTasks.length > 0 &&
|
||||||
if ($[2] !== runningTasks) {
|
runningTasks.every(t => t.type === 'in_process_teammate')
|
||||||
t4 = runningTasks.filter(_temp6).sort(_temp7);
|
|
||||||
$[2] = runningTasks;
|
// Memoize teammate-related computations at the top level (rules of hooks)
|
||||||
$[3] = t4;
|
const teammateEntries = useMemo(
|
||||||
} else {
|
() =>
|
||||||
t4 = $[3];
|
runningTasks
|
||||||
}
|
.filter(
|
||||||
const teammateEntries = t4;
|
(t): t is BackgroundTaskState & { type: 'in_process_teammate' } =>
|
||||||
let t5;
|
t.type === 'in_process_teammate',
|
||||||
if ($[4] !== isLeaderIdle) {
|
)
|
||||||
t5 = {
|
.sort((a, b) =>
|
||||||
name: "main",
|
a.identity.agentName.localeCompare(b.identity.agentName),
|
||||||
|
),
|
||||||
|
[runningTasks],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build array of all pills with their activity state
|
||||||
|
// Each pill is "@{name}" and separator is " " (1 char)
|
||||||
|
// Sort idle agents to the end, but only when not in selection mode
|
||||||
|
// to avoid reordering while user is arrowing through the list
|
||||||
|
// "main" always stays first regardless of idle state
|
||||||
|
const allPills = useMemo(() => {
|
||||||
|
const mainPill = {
|
||||||
|
name: 'main',
|
||||||
color: undefined as keyof Theme | undefined,
|
color: undefined as keyof Theme | undefined,
|
||||||
isIdle: isLeaderIdle,
|
isIdle: isLeaderIdle,
|
||||||
taskId: undefined as string | undefined
|
taskId: undefined as string | undefined,
|
||||||
};
|
}
|
||||||
$[4] = isLeaderIdle;
|
|
||||||
$[5] = t5;
|
const teammatePills = teammateEntries.map(t => ({
|
||||||
} else {
|
name: t.identity.agentName,
|
||||||
t5 = $[5];
|
color: getAgentThemeColor(t.identity.color),
|
||||||
}
|
isIdle: t.isIdle,
|
||||||
const mainPill = t5;
|
taskId: t.id,
|
||||||
let t6;
|
}))
|
||||||
if ($[6] !== mainPill || $[7] !== tasksSelected || $[8] !== teammateEntries) {
|
|
||||||
const teammatePills = teammateEntries.map(_temp8);
|
// Only sort teammates when not selecting to avoid reordering during navigation
|
||||||
if (!tasksSelected) {
|
if (!tasksSelected) {
|
||||||
teammatePills.sort(_temp9);
|
teammatePills.sort((a, b) => {
|
||||||
|
// Active agents first, idle agents last
|
||||||
|
if (a.isIdle !== b.isIdle) return a.isIdle ? 1 : -1
|
||||||
|
return 0 // Keep original order within each group
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const pills = [mainPill, ...teammatePills];
|
|
||||||
t6 = pills.map(_temp0);
|
// main always first, then sorted teammates
|
||||||
$[6] = mainPill;
|
const pills = [mainPill, ...teammatePills]
|
||||||
$[7] = tasksSelected;
|
|
||||||
$[8] = teammateEntries;
|
// Add idx after sorting
|
||||||
$[9] = t6;
|
return pills.map((pill, i) => ({ ...pill, idx: i }))
|
||||||
} else {
|
}, [teammateEntries, isLeaderIdle, tasksSelected])
|
||||||
t6 = $[9];
|
|
||||||
}
|
// Calculate pill widths (including separator space, except first)
|
||||||
const allPills = t6;
|
const pillWidths = useMemo(
|
||||||
let t7;
|
() =>
|
||||||
if ($[10] !== allPills) {
|
allPills.map((pill, i) => {
|
||||||
t7 = allPills.map(_temp1);
|
const pillText = `@${pill.name}`
|
||||||
$[10] = allPills;
|
// First pill has no leading space, others have 1 space separator
|
||||||
$[11] = t7;
|
return stringWidth(pillText) + (i > 0 ? 1 : 0)
|
||||||
} else {
|
}),
|
||||||
t7 = $[11];
|
[allPills],
|
||||||
}
|
)
|
||||||
const pillWidths = t7;
|
|
||||||
if (allTeammates || !showSpinnerTree && isViewingTeammate) {
|
if (allTeammates || (!showSpinnerTree && isViewingTeammate)) {
|
||||||
const selectedIdx = tasksSelected ? teammateFooterIndex : -1;
|
const selectedIdx = tasksSelected ? teammateFooterIndex : -1
|
||||||
let t8;
|
// Which agent is currently foregrounded (bold)
|
||||||
if ($[12] !== teammateEntries || $[13] !== viewingAgentTaskId) {
|
const viewedIdx = viewingAgentTaskId
|
||||||
t8 = viewingAgentTaskId ? teammateEntries.findIndex(t_3 => t_3.id === viewingAgentTaskId) + 1 : 0;
|
? teammateEntries.findIndex(t => t.id === viewingAgentTaskId) + 1
|
||||||
$[12] = teammateEntries;
|
: 0 // 0 = main/leader
|
||||||
$[13] = viewingAgentTaskId;
|
|
||||||
$[14] = t8;
|
// Calculate available width for pills
|
||||||
} else {
|
// Reserve space for: arrows, hint, and minimal padding
|
||||||
t8 = $[14];
|
// Pills are rendered on their own line when in team mode
|
||||||
}
|
const ARROW_WIDTH = 2 // arrow char + space
|
||||||
const viewedIdx = t8;
|
const HINT_WIDTH = 20 // shift+↓ to expand
|
||||||
const availableWidth = Math.max(20, columns - 20 - 4);
|
const PADDING = 4 // minimal safety margin
|
||||||
const t9 = selectedIdx >= 0 ? selectedIdx : 0;
|
const availableWidth = Math.max(20, columns - HINT_WIDTH - PADDING)
|
||||||
let t10;
|
|
||||||
if ($[15] !== availableWidth || $[16] !== pillWidths || $[17] !== t9) {
|
// Calculate visible window of pills
|
||||||
t10 = calculateHorizontalScrollWindow(pillWidths, availableWidth, 2, t9);
|
const { startIndex, endIndex, showLeftArrow, showRightArrow } =
|
||||||
$[15] = availableWidth;
|
calculateHorizontalScrollWindow(
|
||||||
$[16] = pillWidths;
|
pillWidths,
|
||||||
$[17] = t9;
|
availableWidth,
|
||||||
$[18] = t10;
|
ARROW_WIDTH,
|
||||||
} else {
|
selectedIdx >= 0 ? selectedIdx : 0,
|
||||||
t10 = $[18];
|
)
|
||||||
}
|
|
||||||
const {
|
const visiblePills = allPills.slice(startIndex, endIndex)
|
||||||
startIndex,
|
|
||||||
endIndex,
|
return (
|
||||||
showLeftArrow,
|
<>
|
||||||
showRightArrow
|
{showLeftArrow && <Text dimColor>{figures.arrowLeft} </Text>}
|
||||||
} = t10;
|
{visiblePills.map((pill, i) => {
|
||||||
let t11;
|
// First visible pill has no leading separator
|
||||||
if ($[19] !== allPills || $[20] !== endIndex || $[21] !== startIndex) {
|
// (left arrow already provides spacing if present)
|
||||||
t11 = allPills.slice(startIndex, endIndex);
|
const needsSeparator = i > 0
|
||||||
$[19] = allPills;
|
return (
|
||||||
$[20] = endIndex;
|
<React.Fragment key={pill.name}>
|
||||||
$[21] = startIndex;
|
{needsSeparator && <Text> </Text>}
|
||||||
$[22] = t11;
|
<AgentPill
|
||||||
} else {
|
name={pill.name}
|
||||||
t11 = $[22];
|
color={pill.color}
|
||||||
}
|
isSelected={selectedIdx === pill.idx}
|
||||||
const visiblePills = t11;
|
isViewed={viewedIdx === pill.idx}
|
||||||
let t12;
|
isIdle={pill.isIdle}
|
||||||
if ($[23] !== showLeftArrow) {
|
onClick={() =>
|
||||||
t12 = showLeftArrow && <Text dimColor={true}>{figures.arrowLeft} </Text>;
|
pill.taskId
|
||||||
$[23] = showLeftArrow;
|
? enterTeammateView(pill.taskId, setAppState)
|
||||||
$[24] = t12;
|
: exitTeammateView(setAppState)
|
||||||
} else {
|
}
|
||||||
t12 = $[24];
|
/>
|
||||||
}
|
</React.Fragment>
|
||||||
let t13;
|
)
|
||||||
if ($[25] !== selectedIdx || $[26] !== setAppState || $[27] !== viewedIdx || $[28] !== visiblePills) {
|
})}
|
||||||
t13 = visiblePills.map((pill_1, i_1) => {
|
{showRightArrow && <Text dimColor> {figures.arrowRight}</Text>}
|
||||||
const needsSeparator = i_1 > 0;
|
<Text dimColor>
|
||||||
return <React.Fragment key={pill_1.name}>{needsSeparator && <Text> </Text>}<AgentPill name={pill_1.name} color={pill_1.color} isSelected={selectedIdx === pill_1.idx} isViewed={viewedIdx === pill_1.idx} isIdle={pill_1.isIdle} onClick={() => pill_1.taskId ? enterTeammateView(pill_1.taskId, setAppState) : exitTeammateView(setAppState)} /></React.Fragment>;
|
{' · '}
|
||||||
});
|
<KeyboardShortcutHint shortcut="shift + ↓" action="expand" />
|
||||||
$[25] = selectedIdx;
|
</Text>
|
||||||
$[26] = setAppState;
|
</>
|
||||||
$[27] = viewedIdx;
|
)
|
||||||
$[28] = visiblePills;
|
|
||||||
$[29] = t13;
|
|
||||||
} else {
|
|
||||||
t13 = $[29];
|
|
||||||
}
|
|
||||||
let t14;
|
|
||||||
if ($[30] !== showRightArrow) {
|
|
||||||
t14 = showRightArrow && <Text dimColor={true}> {figures.arrowRight}</Text>;
|
|
||||||
$[30] = showRightArrow;
|
|
||||||
$[31] = t14;
|
|
||||||
} else {
|
|
||||||
t14 = $[31];
|
|
||||||
}
|
|
||||||
let t15;
|
|
||||||
if ($[32] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t15 = <Text dimColor={true}>{" \xB7 "}<KeyboardShortcutHint shortcut={"shift + \u2193"} action="expand" /></Text>;
|
|
||||||
$[32] = t15;
|
|
||||||
} else {
|
|
||||||
t15 = $[32];
|
|
||||||
}
|
|
||||||
let t16;
|
|
||||||
if ($[33] !== t12 || $[34] !== t13 || $[35] !== t14) {
|
|
||||||
t16 = <>{t12}{t13}{t14}{t15}</>;
|
|
||||||
$[33] = t12;
|
|
||||||
$[34] = t13;
|
|
||||||
$[35] = t14;
|
|
||||||
$[36] = t16;
|
|
||||||
} else {
|
|
||||||
t16 = $[36];
|
|
||||||
}
|
|
||||||
return t16;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In spinner-tree mode, don't show any footer status for teammates
|
||||||
|
// (they appear in the spinner tree above)
|
||||||
if (shouldHideTasksFooter(tasks ?? {}, showSpinnerTree)) {
|
if (shouldHideTasksFooter(tasks ?? {}, showSpinnerTree)) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runningTasks.length === 0) {
|
if (runningTasks.length === 0) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
let t8;
|
|
||||||
if ($[37] !== runningTasks) {
|
return (
|
||||||
t8 = getPillLabel(runningTasks);
|
<>
|
||||||
$[37] = runningTasks;
|
<SummaryPill selected={tasksSelected} onClick={onOpenDialog}>
|
||||||
$[38] = t8;
|
{getPillLabel(runningTasks)}
|
||||||
} else {
|
</SummaryPill>
|
||||||
t8 = $[38];
|
{pillNeedsCta(runningTasks) && (
|
||||||
}
|
<Text dimColor> · {figures.arrowDown} to view</Text>
|
||||||
let t9;
|
)}
|
||||||
if ($[39] !== onOpenDialog || $[40] !== t8 || $[41] !== tasksSelected) {
|
</>
|
||||||
t9 = <SummaryPill selected={tasksSelected} onClick={onOpenDialog}>{t8}</SummaryPill>;
|
)
|
||||||
$[39] = onOpenDialog;
|
|
||||||
$[40] = t8;
|
|
||||||
$[41] = tasksSelected;
|
|
||||||
$[42] = t9;
|
|
||||||
} else {
|
|
||||||
t9 = $[42];
|
|
||||||
}
|
|
||||||
let t10;
|
|
||||||
if ($[43] !== runningTasks) {
|
|
||||||
t10 = pillNeedsCta(runningTasks) && <Text dimColor={true}> · {figures.arrowDown} to view</Text>;
|
|
||||||
$[43] = runningTasks;
|
|
||||||
$[44] = t10;
|
|
||||||
} else {
|
|
||||||
t10 = $[44];
|
|
||||||
}
|
|
||||||
let t11;
|
|
||||||
if ($[45] !== t10 || $[46] !== t9) {
|
|
||||||
t11 = <>{t9}{t10}</>;
|
|
||||||
$[45] = t10;
|
|
||||||
$[46] = t9;
|
|
||||||
$[47] = t11;
|
|
||||||
} else {
|
|
||||||
t11 = $[47];
|
|
||||||
}
|
|
||||||
return t11;
|
|
||||||
}
|
|
||||||
function _temp1(pill_0, i_0) {
|
|
||||||
const pillText = `@${pill_0.name}`;
|
|
||||||
return stringWidth(pillText) + (i_0 > 0 ? 1 : 0);
|
|
||||||
}
|
|
||||||
function _temp0(pill, i) {
|
|
||||||
return {
|
|
||||||
...pill,
|
|
||||||
idx: i
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function _temp9(a_0, b_0) {
|
|
||||||
if (a_0.isIdle !== b_0.isIdle) {
|
|
||||||
return a_0.isIdle ? 1 : -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
function _temp8(t_2) {
|
|
||||||
return {
|
|
||||||
name: t_2.identity.agentName,
|
|
||||||
color: getAgentThemeColor(t_2.identity.color),
|
|
||||||
isIdle: t_2.isIdle,
|
|
||||||
taskId: t_2.id
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function _temp7(a, b) {
|
|
||||||
return a.identity.agentName.localeCompare(b.identity.agentName);
|
|
||||||
}
|
|
||||||
function _temp6(t_1) {
|
|
||||||
return t_1.type === "in_process_teammate";
|
|
||||||
}
|
|
||||||
function _temp5(t_0) {
|
|
||||||
return t_0.type === "in_process_teammate";
|
|
||||||
}
|
|
||||||
function _temp4(s_1) {
|
|
||||||
return s_1.expandedView;
|
|
||||||
}
|
|
||||||
function _temp3(t) {
|
|
||||||
return isBackgroundTask(t) && !(false && isPanelAgentTask(t));
|
|
||||||
}
|
|
||||||
function _temp2(s_0) {
|
|
||||||
return s_0.viewingAgentTaskId;
|
|
||||||
}
|
|
||||||
function _temp(s) {
|
|
||||||
return s.tasks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentPillProps = {
|
type AgentPillProps = {
|
||||||
name: string;
|
name: string
|
||||||
color?: keyof Theme;
|
color?: keyof Theme
|
||||||
isSelected: boolean;
|
isSelected: boolean
|
||||||
isViewed: boolean;
|
isViewed: boolean
|
||||||
isIdle: boolean;
|
isIdle: boolean
|
||||||
onClick?: () => void;
|
onClick?: () => void
|
||||||
};
|
}
|
||||||
function AgentPill(t0) {
|
|
||||||
const $ = _c(19);
|
function AgentPill({
|
||||||
const {
|
name,
|
||||||
name,
|
color,
|
||||||
color,
|
isSelected,
|
||||||
isSelected,
|
isViewed,
|
||||||
isViewed,
|
isIdle,
|
||||||
isIdle,
|
onClick,
|
||||||
onClick
|
}: AgentPillProps): React.ReactNode {
|
||||||
} = t0;
|
const [hover, setHover] = useState(false)
|
||||||
const [hover, setHover] = useState(false);
|
// Hover mirrors the keyboard-selected look so the affordance is familiar.
|
||||||
const highlighted = isSelected || hover;
|
const highlighted = isSelected || hover
|
||||||
let label;
|
|
||||||
|
let label: React.ReactNode
|
||||||
if (highlighted) {
|
if (highlighted) {
|
||||||
let t1;
|
label = color ? (
|
||||||
if ($[0] !== color || $[1] !== isViewed || $[2] !== name) {
|
<Text backgroundColor={color} color="inverseText" bold={isViewed}>
|
||||||
t1 = color ? <Text backgroundColor={color} color="inverseText" bold={isViewed}>@{name}</Text> : <Text color="background" inverse={true} bold={isViewed}>@{name}</Text>;
|
@{name}
|
||||||
$[0] = color;
|
</Text>
|
||||||
$[1] = isViewed;
|
) : (
|
||||||
$[2] = name;
|
<Text color="background" inverse bold={isViewed}>
|
||||||
$[3] = t1;
|
@{name}
|
||||||
} else {
|
</Text>
|
||||||
t1 = $[3];
|
)
|
||||||
}
|
} else if (isIdle) {
|
||||||
label = t1;
|
label = (
|
||||||
|
<Text dimColor bold={isViewed}>
|
||||||
|
@{name}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
} else if (isViewed) {
|
||||||
|
label = (
|
||||||
|
<Text color={color} bold>
|
||||||
|
@{name}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
if (isIdle) {
|
label = (
|
||||||
let t1;
|
<Text color={color} dimColor={!color}>
|
||||||
if ($[4] !== isViewed || $[5] !== name) {
|
@{name}
|
||||||
t1 = <Text dimColor={true} bold={isViewed}>@{name}</Text>;
|
</Text>
|
||||||
$[4] = isViewed;
|
)
|
||||||
$[5] = name;
|
|
||||||
$[6] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[6];
|
|
||||||
}
|
|
||||||
label = t1;
|
|
||||||
} else {
|
|
||||||
if (isViewed) {
|
|
||||||
let t1;
|
|
||||||
if ($[7] !== color || $[8] !== name) {
|
|
||||||
t1 = <Text color={color} bold={true}>@{name}</Text>;
|
|
||||||
$[7] = color;
|
|
||||||
$[8] = name;
|
|
||||||
$[9] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[9];
|
|
||||||
}
|
|
||||||
label = t1;
|
|
||||||
} else {
|
|
||||||
const t1 = !color;
|
|
||||||
let t2;
|
|
||||||
if ($[10] !== color || $[11] !== name || $[12] !== t1) {
|
|
||||||
t2 = <Text color={color} dimColor={t1}>@{name}</Text>;
|
|
||||||
$[10] = color;
|
|
||||||
$[11] = name;
|
|
||||||
$[12] = t1;
|
|
||||||
$[13] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[13];
|
|
||||||
}
|
|
||||||
label = t2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!onClick) {
|
|
||||||
return label;
|
if (!onClick) return label
|
||||||
}
|
return (
|
||||||
let t1;
|
<Box
|
||||||
let t2;
|
onClick={onClick}
|
||||||
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
|
onMouseEnter={() => setHover(true)}
|
||||||
t1 = () => setHover(true);
|
onMouseLeave={() => setHover(false)}
|
||||||
t2 = () => setHover(false);
|
>
|
||||||
$[14] = t1;
|
{label}
|
||||||
$[15] = t2;
|
</Box>
|
||||||
} else {
|
)
|
||||||
t1 = $[14];
|
|
||||||
t2 = $[15];
|
|
||||||
}
|
|
||||||
let t3;
|
|
||||||
if ($[16] !== label || $[17] !== onClick) {
|
|
||||||
t3 = <Box onClick={onClick} onMouseEnter={t1} onMouseLeave={t2}>{label}</Box>;
|
|
||||||
$[16] = label;
|
|
||||||
$[17] = onClick;
|
|
||||||
$[18] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[18];
|
|
||||||
}
|
|
||||||
return t3;
|
|
||||||
}
|
}
|
||||||
function SummaryPill(t0) {
|
|
||||||
const $ = _c(8);
|
function SummaryPill({
|
||||||
const {
|
selected,
|
||||||
selected,
|
onClick,
|
||||||
onClick,
|
children,
|
||||||
children
|
}: {
|
||||||
} = t0;
|
selected: boolean
|
||||||
const [hover, setHover] = useState(false);
|
onClick?: () => void
|
||||||
const t1 = selected || hover;
|
children: React.ReactNode
|
||||||
let t2;
|
}): React.ReactNode {
|
||||||
if ($[0] !== children || $[1] !== t1) {
|
const [hover, setHover] = useState(false)
|
||||||
t2 = <Text color="background" inverse={t1}>{children}</Text>;
|
const label = (
|
||||||
$[0] = children;
|
<Text color="background" inverse={selected || hover}>
|
||||||
$[1] = t1;
|
{children}
|
||||||
$[2] = t2;
|
</Text>
|
||||||
} else {
|
)
|
||||||
t2 = $[2];
|
if (!onClick) return label
|
||||||
}
|
return (
|
||||||
const label = t2;
|
<Box
|
||||||
if (!onClick) {
|
onClick={onClick}
|
||||||
return label;
|
onMouseEnter={() => setHover(true)}
|
||||||
}
|
onMouseLeave={() => setHover(false)}
|
||||||
let t3;
|
>
|
||||||
let t4;
|
{label}
|
||||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
</Box>
|
||||||
t3 = () => setHover(true);
|
)
|
||||||
t4 = () => setHover(false);
|
|
||||||
$[3] = t3;
|
|
||||||
$[4] = t4;
|
|
||||||
} else {
|
|
||||||
t3 = $[3];
|
|
||||||
t4 = $[4];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[5] !== label || $[6] !== onClick) {
|
|
||||||
t5 = <Box onClick={onClick} onMouseEnter={t3} onMouseLeave={t4}>{label}</Box>;
|
|
||||||
$[5] = label;
|
|
||||||
$[6] = onClick;
|
|
||||||
$[7] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[7];
|
|
||||||
}
|
|
||||||
return t5;
|
|
||||||
}
|
}
|
||||||
function getAgentThemeColor(colorName: string | undefined): keyof Theme | undefined {
|
|
||||||
if (!colorName) return undefined;
|
function getAgentThemeColor(
|
||||||
|
colorName: string | undefined,
|
||||||
|
): keyof Theme | undefined {
|
||||||
|
if (!colorName) return undefined
|
||||||
if (AGENT_COLORS.includes(colorName as AgentColorName)) {
|
if (AGENT_COLORS.includes(colorName as AgentColorName)) {
|
||||||
return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName];
|
return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,250 +1,136 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React from 'react'
|
||||||
import React from 'react';
|
import type { DeepImmutable } from 'src/types/utils.js'
|
||||||
import type { DeepImmutable } from 'src/types/utils.js';
|
import { useElapsedTime } from '../../hooks/useElapsedTime.js'
|
||||||
import { useElapsedTime } from '../../hooks/useElapsedTime.js';
|
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
import type { DreamTaskState } from '../../tasks/DreamTask/DreamTask.js'
|
||||||
import type { DreamTaskState } from '../../tasks/DreamTask/DreamTask.js';
|
import { plural } from '../../utils/stringUtils.js'
|
||||||
import { plural } from '../../utils/stringUtils.js';
|
import { Byline } from '../design-system/Byline.js'
|
||||||
import { Byline } from '../design-system/Byline.js';
|
import { Dialog } from '../design-system/Dialog.js'
|
||||||
import { Dialog } from '../design-system/Dialog.js';
|
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
task: DeepImmutable<DreamTaskState>;
|
task: DeepImmutable<DreamTaskState>
|
||||||
onDone: () => void;
|
onDone: () => void
|
||||||
onBack?: () => void;
|
onBack?: () => void
|
||||||
onKill?: () => void;
|
onKill?: () => void
|
||||||
};
|
}
|
||||||
|
|
||||||
// How many recent turns to render. Earlier turns collapse to a count.
|
// How many recent turns to render. Earlier turns collapse to a count.
|
||||||
const VISIBLE_TURNS = 6;
|
const VISIBLE_TURNS = 6
|
||||||
export function DreamDetailDialog(t0) {
|
|
||||||
const $ = _c(70);
|
export function DreamDetailDialog({
|
||||||
const {
|
task,
|
||||||
task,
|
onDone,
|
||||||
onDone,
|
onBack,
|
||||||
onBack,
|
onKill,
|
||||||
onKill
|
}: Props): React.ReactNode {
|
||||||
} = t0;
|
const elapsedTime = useElapsedTime(
|
||||||
const elapsedTime = useElapsedTime(task.startTime, task.status === "running", 1000, 0);
|
task.startTime,
|
||||||
let t1;
|
task.status === 'running',
|
||||||
if ($[0] !== onDone) {
|
1000,
|
||||||
t1 = {
|
0,
|
||||||
"confirm:yes": onDone
|
)
|
||||||
};
|
|
||||||
$[0] = onDone;
|
// Dialog handles confirm:no (Esc) → onCancel. Wire confirm:yes (Enter/y) too.
|
||||||
$[1] = t1;
|
useKeybindings({ 'confirm:yes': onDone }, { context: 'Confirmation' })
|
||||||
} else {
|
|
||||||
t1 = $[1];
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
onDone()
|
||||||
|
} else if (e.key === 'left' && onBack) {
|
||||||
|
e.preventDefault()
|
||||||
|
onBack()
|
||||||
|
} else if (e.key === 'x' && task.status === 'running' && onKill) {
|
||||||
|
e.preventDefault()
|
||||||
|
onKill()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let t2;
|
|
||||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
// Turns with text to show. Tool-only turns (text='') are dropped entirely —
|
||||||
t2 = {
|
// the per-turn toolUseCount already captures that work.
|
||||||
context: "Confirmation"
|
const visibleTurns = task.turns.filter(t => t.text !== '')
|
||||||
};
|
const shown = visibleTurns.slice(-VISIBLE_TURNS)
|
||||||
$[2] = t2;
|
const hidden = visibleTurns.length - shown.length
|
||||||
} else {
|
|
||||||
t2 = $[2];
|
return (
|
||||||
}
|
<Box
|
||||||
useKeybindings(t1, t2);
|
flexDirection="column"
|
||||||
let t3;
|
tabIndex={0}
|
||||||
if ($[3] !== onBack || $[4] !== onDone || $[5] !== onKill || $[6] !== task.status) {
|
autoFocus
|
||||||
t3 = e => {
|
onKeyDown={handleKeyDown}
|
||||||
if (e.key === " ") {
|
>
|
||||||
e.preventDefault();
|
<Dialog
|
||||||
onDone();
|
title="Memory consolidation"
|
||||||
} else {
|
subtitle={
|
||||||
if (e.key === "left" && onBack) {
|
<Text dimColor>
|
||||||
e.preventDefault();
|
{elapsedTime} · reviewing {task.sessionsReviewing}{' '}
|
||||||
onBack();
|
{plural(task.sessionsReviewing, 'session')}
|
||||||
} else {
|
{task.filesTouched.length > 0 && (
|
||||||
if (e.key === "x" && task.status === "running" && onKill) {
|
<>
|
||||||
e.preventDefault();
|
{' '}
|
||||||
onKill();
|
· {task.filesTouched.length}{' '}
|
||||||
}
|
{plural(task.filesTouched.length, 'file')} touched
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
}
|
}
|
||||||
}
|
onCancel={onDone}
|
||||||
};
|
color="background"
|
||||||
$[3] = onBack;
|
inputGuide={exitState =>
|
||||||
$[4] = onDone;
|
exitState.pending ? (
|
||||||
$[5] = onKill;
|
<Text>Press {exitState.keyName} again to exit</Text>
|
||||||
$[6] = task.status;
|
) : (
|
||||||
$[7] = t3;
|
<Byline>
|
||||||
} else {
|
{onBack && <KeyboardShortcutHint shortcut="←" action="go back" />}
|
||||||
t3 = $[7];
|
<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />
|
||||||
}
|
{task.status === 'running' && onKill && (
|
||||||
const handleKeyDown = t3;
|
<KeyboardShortcutHint shortcut="x" action="stop" />
|
||||||
let T0;
|
)}
|
||||||
let T1;
|
</Byline>
|
||||||
let T2;
|
)
|
||||||
let t10;
|
}
|
||||||
let t11;
|
>
|
||||||
let t12;
|
<Box flexDirection="column" gap={1}>
|
||||||
let t13;
|
<Text>
|
||||||
let t14;
|
<Text bold>Status:</Text>{' '}
|
||||||
let t15;
|
{task.status === 'running' ? (
|
||||||
let t16;
|
<Text color="background">running</Text>
|
||||||
let t4;
|
) : task.status === 'completed' ? (
|
||||||
let t5;
|
<Text color="success">{task.status}</Text>
|
||||||
let t6;
|
) : (
|
||||||
let t7;
|
<Text color="error">{task.status}</Text>
|
||||||
let t8;
|
)}
|
||||||
let t9;
|
</Text>
|
||||||
if ($[8] !== elapsedTime || $[9] !== handleKeyDown || $[10] !== onBack || $[11] !== onDone || $[12] !== onKill || $[13] !== task.filesTouched.length || $[14] !== task.sessionsReviewing || $[15] !== task.status || $[16] !== task.turns) {
|
|
||||||
const visibleTurns = task.turns.filter(_temp);
|
{shown.length === 0 ? (
|
||||||
const shown = visibleTurns.slice(-VISIBLE_TURNS);
|
<Text dimColor>
|
||||||
const hidden = visibleTurns.length - shown.length;
|
{task.status === 'running' ? 'Starting…' : '(no text output)'}
|
||||||
T2 = Box;
|
</Text>
|
||||||
t13 = "column";
|
) : (
|
||||||
t14 = 0;
|
<>
|
||||||
t15 = true;
|
{hidden > 0 && (
|
||||||
t16 = handleKeyDown;
|
<Text dimColor>
|
||||||
T1 = Dialog;
|
({hidden} earlier {plural(hidden, 'turn')})
|
||||||
t8 = "Memory consolidation";
|
</Text>
|
||||||
const t17 = task.sessionsReviewing;
|
)}
|
||||||
let t18;
|
{shown.map((turn, i) => (
|
||||||
if ($[33] !== task.sessionsReviewing) {
|
<Box key={i} flexDirection="column">
|
||||||
t18 = plural(task.sessionsReviewing, "session");
|
<Text wrap="wrap">{turn.text}</Text>
|
||||||
$[33] = task.sessionsReviewing;
|
{turn.toolUseCount > 0 && (
|
||||||
$[34] = t18;
|
<Text dimColor>
|
||||||
} else {
|
{' '}({turn.toolUseCount}{' '}
|
||||||
t18 = $[34];
|
{plural(turn.toolUseCount, 'tool')})
|
||||||
}
|
</Text>
|
||||||
let t19;
|
)}
|
||||||
if ($[35] !== task.filesTouched.length) {
|
</Box>
|
||||||
t19 = task.filesTouched.length > 0 && <>{" "}· {task.filesTouched.length}{" "}{plural(task.filesTouched.length, "file")} touched</>;
|
))}
|
||||||
$[35] = task.filesTouched.length;
|
</>
|
||||||
$[36] = t19;
|
)}
|
||||||
} else {
|
</Box>
|
||||||
t19 = $[36];
|
</Dialog>
|
||||||
}
|
</Box>
|
||||||
if ($[37] !== elapsedTime || $[38] !== t18 || $[39] !== t19 || $[40] !== task.sessionsReviewing) {
|
)
|
||||||
t9 = <Text dimColor={true}>{elapsedTime} · reviewing {t17}{" "}{t18}{t19}</Text>;
|
|
||||||
$[37] = elapsedTime;
|
|
||||||
$[38] = t18;
|
|
||||||
$[39] = t19;
|
|
||||||
$[40] = task.sessionsReviewing;
|
|
||||||
$[41] = t9;
|
|
||||||
} else {
|
|
||||||
t9 = $[41];
|
|
||||||
}
|
|
||||||
t10 = onDone;
|
|
||||||
t11 = "background";
|
|
||||||
if ($[42] !== onBack || $[43] !== onKill || $[44] !== task.status) {
|
|
||||||
t12 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{task.status === "running" && onKill && <KeyboardShortcutHint shortcut="x" action="stop" />}</Byline>;
|
|
||||||
$[42] = onBack;
|
|
||||||
$[43] = onKill;
|
|
||||||
$[44] = task.status;
|
|
||||||
$[45] = t12;
|
|
||||||
} else {
|
|
||||||
t12 = $[45];
|
|
||||||
}
|
|
||||||
T0 = Box;
|
|
||||||
t4 = "column";
|
|
||||||
t5 = 1;
|
|
||||||
let t20;
|
|
||||||
if ($[46] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t20 = <Text bold={true}>Status:</Text>;
|
|
||||||
$[46] = t20;
|
|
||||||
} else {
|
|
||||||
t20 = $[46];
|
|
||||||
}
|
|
||||||
if ($[47] !== task.status) {
|
|
||||||
t6 = <Text>{t20}{" "}{task.status === "running" ? <Text color="background">running</Text> : task.status === "completed" ? <Text color="success">{task.status}</Text> : <Text color="error">{task.status}</Text>}</Text>;
|
|
||||||
$[47] = task.status;
|
|
||||||
$[48] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[48];
|
|
||||||
}
|
|
||||||
t7 = shown.length === 0 ? <Text dimColor={true}>{task.status === "running" ? "Starting\u2026" : "(no text output)"}</Text> : <>{hidden > 0 && <Text dimColor={true}>({hidden} earlier {plural(hidden, "turn")})</Text>}{shown.map(_temp2)}</>;
|
|
||||||
$[8] = elapsedTime;
|
|
||||||
$[9] = handleKeyDown;
|
|
||||||
$[10] = onBack;
|
|
||||||
$[11] = onDone;
|
|
||||||
$[12] = onKill;
|
|
||||||
$[13] = task.filesTouched.length;
|
|
||||||
$[14] = task.sessionsReviewing;
|
|
||||||
$[15] = task.status;
|
|
||||||
$[16] = task.turns;
|
|
||||||
$[17] = T0;
|
|
||||||
$[18] = T1;
|
|
||||||
$[19] = T2;
|
|
||||||
$[20] = t10;
|
|
||||||
$[21] = t11;
|
|
||||||
$[22] = t12;
|
|
||||||
$[23] = t13;
|
|
||||||
$[24] = t14;
|
|
||||||
$[25] = t15;
|
|
||||||
$[26] = t16;
|
|
||||||
$[27] = t4;
|
|
||||||
$[28] = t5;
|
|
||||||
$[29] = t6;
|
|
||||||
$[30] = t7;
|
|
||||||
$[31] = t8;
|
|
||||||
$[32] = t9;
|
|
||||||
} else {
|
|
||||||
T0 = $[17];
|
|
||||||
T1 = $[18];
|
|
||||||
T2 = $[19];
|
|
||||||
t10 = $[20];
|
|
||||||
t11 = $[21];
|
|
||||||
t12 = $[22];
|
|
||||||
t13 = $[23];
|
|
||||||
t14 = $[24];
|
|
||||||
t15 = $[25];
|
|
||||||
t16 = $[26];
|
|
||||||
t4 = $[27];
|
|
||||||
t5 = $[28];
|
|
||||||
t6 = $[29];
|
|
||||||
t7 = $[30];
|
|
||||||
t8 = $[31];
|
|
||||||
t9 = $[32];
|
|
||||||
}
|
|
||||||
let t17;
|
|
||||||
if ($[49] !== T0 || $[50] !== t4 || $[51] !== t5 || $[52] !== t6 || $[53] !== t7) {
|
|
||||||
t17 = <T0 flexDirection={t4} gap={t5}>{t6}{t7}</T0>;
|
|
||||||
$[49] = T0;
|
|
||||||
$[50] = t4;
|
|
||||||
$[51] = t5;
|
|
||||||
$[52] = t6;
|
|
||||||
$[53] = t7;
|
|
||||||
$[54] = t17;
|
|
||||||
} else {
|
|
||||||
t17 = $[54];
|
|
||||||
}
|
|
||||||
let t18;
|
|
||||||
if ($[55] !== T1 || $[56] !== t10 || $[57] !== t11 || $[58] !== t12 || $[59] !== t17 || $[60] !== t8 || $[61] !== t9) {
|
|
||||||
t18 = <T1 title={t8} subtitle={t9} onCancel={t10} color={t11} inputGuide={t12}>{t17}</T1>;
|
|
||||||
$[55] = T1;
|
|
||||||
$[56] = t10;
|
|
||||||
$[57] = t11;
|
|
||||||
$[58] = t12;
|
|
||||||
$[59] = t17;
|
|
||||||
$[60] = t8;
|
|
||||||
$[61] = t9;
|
|
||||||
$[62] = t18;
|
|
||||||
} else {
|
|
||||||
t18 = $[62];
|
|
||||||
}
|
|
||||||
let t19;
|
|
||||||
if ($[63] !== T2 || $[64] !== t13 || $[65] !== t14 || $[66] !== t15 || $[67] !== t16 || $[68] !== t18) {
|
|
||||||
t19 = <T2 flexDirection={t13} tabIndex={t14} autoFocus={t15} onKeyDown={t16}>{t18}</T2>;
|
|
||||||
$[63] = T2;
|
|
||||||
$[64] = t13;
|
|
||||||
$[65] = t14;
|
|
||||||
$[66] = t15;
|
|
||||||
$[67] = t16;
|
|
||||||
$[68] = t18;
|
|
||||||
$[69] = t19;
|
|
||||||
} else {
|
|
||||||
t19 = $[69];
|
|
||||||
}
|
|
||||||
return t19;
|
|
||||||
}
|
|
||||||
function _temp2(turn, i) {
|
|
||||||
return <Box key={i} flexDirection="column"><Text wrap="wrap">{turn.text}</Text>{turn.toolUseCount > 0 && <Text dimColor={true}>{" "}({turn.toolUseCount}{" "}{plural(turn.toolUseCount, "tool")})</Text>}</Box>;
|
|
||||||
}
|
|
||||||
function _temp(t) {
|
|
||||||
return t.text !== "";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,265 +1,193 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { useMemo } from 'react'
|
||||||
import React, { useMemo } from 'react';
|
import type { DeepImmutable } from 'src/types/utils.js'
|
||||||
import type { DeepImmutable } from 'src/types/utils.js';
|
import { useElapsedTime } from '../../hooks/useElapsedTime.js'
|
||||||
import { useElapsedTime } from '../../hooks/useElapsedTime.js';
|
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
import { Box, Text, useTheme } from '../../ink.js'
|
||||||
import { Box, Text, useTheme } from '../../ink.js';
|
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
import { getEmptyToolPermissionContext } from '../../Tool.js'
|
||||||
import { getEmptyToolPermissionContext } from '../../Tool.js';
|
import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js'
|
||||||
import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js';
|
import { getTools } from '../../tools.js'
|
||||||
import { getTools } from '../../tools.js';
|
import { formatNumber, truncateToWidth } from '../../utils/format.js'
|
||||||
import { formatNumber, truncateToWidth } from '../../utils/format.js';
|
import { toInkColor } from '../../utils/ink.js'
|
||||||
import { toInkColor } from '../../utils/ink.js';
|
import { Byline } from '../design-system/Byline.js'
|
||||||
import { Byline } from '../design-system/Byline.js';
|
import { Dialog } from '../design-system/Dialog.js'
|
||||||
import { Dialog } from '../design-system/Dialog.js';
|
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
import { renderToolActivity } from './renderToolActivity.js'
|
||||||
import { renderToolActivity } from './renderToolActivity.js';
|
import { describeTeammateActivity } from './taskStatusUtils.js'
|
||||||
import { describeTeammateActivity } from './taskStatusUtils.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
teammate: DeepImmutable<InProcessTeammateTaskState>;
|
teammate: DeepImmutable<InProcessTeammateTaskState>
|
||||||
onDone: () => void;
|
onDone: () => void
|
||||||
onKill?: () => void;
|
onKill?: () => void
|
||||||
onBack?: () => void;
|
onBack?: () => void
|
||||||
onForeground?: () => void;
|
onForeground?: () => void
|
||||||
};
|
}
|
||||||
export function InProcessTeammateDetailDialog(t0) {
|
export function InProcessTeammateDetailDialog({
|
||||||
const $ = _c(63);
|
teammate,
|
||||||
const {
|
onDone,
|
||||||
teammate,
|
onKill,
|
||||||
onDone,
|
onBack,
|
||||||
onKill,
|
onForeground,
|
||||||
onBack,
|
}: Props): React.ReactNode {
|
||||||
onForeground
|
const [theme] = useTheme()
|
||||||
} = t0;
|
const tools = useMemo(() => getTools(getEmptyToolPermissionContext()), [])
|
||||||
const [theme] = useTheme();
|
|
||||||
let t1;
|
const elapsedTime = useElapsedTime(
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
teammate.startTime,
|
||||||
t1 = getTools(getEmptyToolPermissionContext());
|
teammate.status === 'running',
|
||||||
$[0] = t1;
|
1000,
|
||||||
} else {
|
teammate.totalPausedMs ?? 0,
|
||||||
t1 = $[0];
|
)
|
||||||
}
|
|
||||||
const tools = t1;
|
// Restore confirm:yes (Enter/y) dismissal — Dialog handles confirm:no (Esc)
|
||||||
const elapsedTime = useElapsedTime(teammate.startTime, teammate.status === "running", 1000, teammate.totalPausedMs ?? 0);
|
useKeybindings(
|
||||||
let t2;
|
{
|
||||||
if ($[1] !== onDone) {
|
'confirm:yes': onDone,
|
||||||
t2 = {
|
},
|
||||||
"confirm:yes": onDone
|
{ context: 'Confirmation' },
|
||||||
};
|
)
|
||||||
$[1] = onDone;
|
|
||||||
$[2] = t2;
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
} else {
|
if (e.key === ' ') {
|
||||||
t2 = $[2];
|
e.preventDefault()
|
||||||
}
|
onDone()
|
||||||
let t3;
|
} else if (e.key === 'left' && onBack) {
|
||||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
e.preventDefault()
|
||||||
t3 = {
|
onBack()
|
||||||
context: "Confirmation"
|
} else if (e.key === 'x' && teammate.status === 'running' && onKill) {
|
||||||
};
|
e.preventDefault()
|
||||||
$[3] = t3;
|
onKill()
|
||||||
} else {
|
} else if (e.key === 'f' && teammate.status === 'running' && onForeground) {
|
||||||
t3 = $[3];
|
e.preventDefault()
|
||||||
}
|
onForeground()
|
||||||
useKeybindings(t2, t3);
|
}
|
||||||
let t4;
|
}
|
||||||
if ($[4] !== onBack || $[5] !== onDone || $[6] !== onForeground || $[7] !== onKill || $[8] !== teammate.status) {
|
|
||||||
t4 = e => {
|
const activity = describeTeammateActivity(teammate)
|
||||||
if (e.key === " ") {
|
|
||||||
e.preventDefault();
|
const tokenCount =
|
||||||
onDone();
|
teammate.result?.totalTokens ?? teammate.progress?.tokenCount
|
||||||
} else {
|
const toolUseCount =
|
||||||
if (e.key === "left" && onBack) {
|
teammate.result?.totalToolUseCount ?? teammate.progress?.toolUseCount
|
||||||
e.preventDefault();
|
|
||||||
onBack();
|
const displayPrompt = truncateToWidth(teammate.prompt, 300)
|
||||||
} else {
|
|
||||||
if (e.key === "x" && teammate.status === "running" && onKill) {
|
const title = (
|
||||||
e.preventDefault();
|
<Text>
|
||||||
onKill();
|
<Text color={toInkColor(teammate.identity.color)}>
|
||||||
} else {
|
@{teammate.identity.agentName}
|
||||||
if (e.key === "f" && teammate.status === "running" && onForeground) {
|
</Text>
|
||||||
e.preventDefault();
|
{activity && <Text dimColor> ({activity})</Text>}
|
||||||
onForeground();
|
</Text>
|
||||||
}
|
)
|
||||||
}
|
|
||||||
}
|
const subtitle = (
|
||||||
}
|
<Text>
|
||||||
};
|
{teammate.status !== 'running' && (
|
||||||
$[4] = onBack;
|
<Text
|
||||||
$[5] = onDone;
|
color={
|
||||||
$[6] = onForeground;
|
teammate.status === 'completed'
|
||||||
$[7] = onKill;
|
? 'success'
|
||||||
$[8] = teammate.status;
|
: teammate.status === 'killed'
|
||||||
$[9] = t4;
|
? 'warning'
|
||||||
} else {
|
: 'error'
|
||||||
t4 = $[9];
|
}
|
||||||
}
|
>
|
||||||
const handleKeyDown = t4;
|
{teammate.status === 'completed'
|
||||||
let t5;
|
? 'Completed'
|
||||||
if ($[10] !== teammate) {
|
: teammate.status === 'failed'
|
||||||
t5 = describeTeammateActivity(teammate);
|
? 'Failed'
|
||||||
$[10] = teammate;
|
: 'Stopped'}
|
||||||
$[11] = t5;
|
{' · '}
|
||||||
} else {
|
</Text>
|
||||||
t5 = $[11];
|
)}
|
||||||
}
|
<Text dimColor>
|
||||||
const activity = t5;
|
{elapsedTime}
|
||||||
const tokenCount = teammate.result?.totalTokens ?? teammate.progress?.tokenCount;
|
{tokenCount !== undefined && tokenCount > 0 && (
|
||||||
const toolUseCount = teammate.result?.totalToolUseCount ?? teammate.progress?.toolUseCount;
|
<> · {formatNumber(tokenCount)} tokens</>
|
||||||
let t6;
|
)}
|
||||||
if ($[12] !== teammate.prompt) {
|
{toolUseCount !== undefined && toolUseCount > 0 && (
|
||||||
t6 = truncateToWidth(teammate.prompt, 300);
|
<>
|
||||||
$[12] = teammate.prompt;
|
{' '}
|
||||||
$[13] = t6;
|
· {toolUseCount} {toolUseCount === 1 ? 'tool' : 'tools'}
|
||||||
} else {
|
</>
|
||||||
t6 = $[13];
|
)}
|
||||||
}
|
</Text>
|
||||||
const displayPrompt = t6;
|
</Text>
|
||||||
let t7;
|
)
|
||||||
if ($[14] !== teammate.identity.color) {
|
|
||||||
t7 = toInkColor(teammate.identity.color);
|
return (
|
||||||
$[14] = teammate.identity.color;
|
<Box
|
||||||
$[15] = t7;
|
flexDirection="column"
|
||||||
} else {
|
tabIndex={0}
|
||||||
t7 = $[15];
|
autoFocus
|
||||||
}
|
onKeyDown={handleKeyDown}
|
||||||
let t8;
|
>
|
||||||
if ($[16] !== t7 || $[17] !== teammate.identity.agentName) {
|
<Dialog
|
||||||
t8 = <Text color={t7}>@{teammate.identity.agentName}</Text>;
|
title={title}
|
||||||
$[16] = t7;
|
subtitle={subtitle}
|
||||||
$[17] = teammate.identity.agentName;
|
onCancel={onDone}
|
||||||
$[18] = t8;
|
color="background"
|
||||||
} else {
|
inputGuide={exitState =>
|
||||||
t8 = $[18];
|
exitState.pending ? (
|
||||||
}
|
<Text>Press {exitState.keyName} again to exit</Text>
|
||||||
let t9;
|
) : (
|
||||||
if ($[19] !== activity) {
|
<Byline>
|
||||||
t9 = activity && <Text dimColor={true}> ({activity})</Text>;
|
{onBack && <KeyboardShortcutHint shortcut="←" action="go back" />}
|
||||||
$[19] = activity;
|
<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />
|
||||||
$[20] = t9;
|
{teammate.status === 'running' && onKill && (
|
||||||
} else {
|
<KeyboardShortcutHint shortcut="x" action="stop" />
|
||||||
t9 = $[20];
|
)}
|
||||||
}
|
{teammate.status === 'running' && onForeground && (
|
||||||
let t10;
|
<KeyboardShortcutHint shortcut="f" action="foreground" />
|
||||||
if ($[21] !== t8 || $[22] !== t9) {
|
)}
|
||||||
t10 = <Text>{t8}{t9}</Text>;
|
</Byline>
|
||||||
$[21] = t8;
|
)
|
||||||
$[22] = t9;
|
}
|
||||||
$[23] = t10;
|
>
|
||||||
} else {
|
{/* Recent activities for running teammates */}
|
||||||
t10 = $[23];
|
{teammate.status === 'running' &&
|
||||||
}
|
teammate.progress?.recentActivities &&
|
||||||
const title = t10;
|
teammate.progress.recentActivities.length > 0 && (
|
||||||
let t11;
|
<Box flexDirection="column">
|
||||||
if ($[24] !== teammate.status) {
|
<Text bold dimColor>
|
||||||
t11 = teammate.status !== "running" && <Text color={teammate.status === "completed" ? "success" : teammate.status === "killed" ? "warning" : "error"}>{teammate.status === "completed" ? "Completed" : teammate.status === "failed" ? "Failed" : "Stopped"}{" \xB7 "}</Text>;
|
Progress
|
||||||
$[24] = teammate.status;
|
</Text>
|
||||||
$[25] = t11;
|
{teammate.progress.recentActivities.map((activity, i) => (
|
||||||
} else {
|
<Text
|
||||||
t11 = $[25];
|
key={i}
|
||||||
}
|
dimColor={i < teammate.progress!.recentActivities!.length - 1}
|
||||||
let t12;
|
wrap="truncate-end"
|
||||||
if ($[26] !== tokenCount) {
|
>
|
||||||
t12 = tokenCount !== undefined && tokenCount > 0 && <> · {formatNumber(tokenCount)} tokens</>;
|
{i === teammate.progress!.recentActivities!.length - 1
|
||||||
$[26] = tokenCount;
|
? '› '
|
||||||
$[27] = t12;
|
: ' '}
|
||||||
} else {
|
{renderToolActivity(activity, tools, theme)}
|
||||||
t12 = $[27];
|
</Text>
|
||||||
}
|
))}
|
||||||
let t13;
|
</Box>
|
||||||
if ($[28] !== toolUseCount) {
|
)}
|
||||||
t13 = toolUseCount !== undefined && toolUseCount > 0 && <>{" "}· {toolUseCount} {toolUseCount === 1 ? "tool" : "tools"}</>;
|
|
||||||
$[28] = toolUseCount;
|
{/* Prompt section */}
|
||||||
$[29] = t13;
|
<Box flexDirection="column" marginTop={1}>
|
||||||
} else {
|
<Text bold dimColor>
|
||||||
t13 = $[29];
|
Prompt
|
||||||
}
|
</Text>
|
||||||
let t14;
|
<Text wrap="wrap">{displayPrompt}</Text>
|
||||||
if ($[30] !== elapsedTime || $[31] !== t12 || $[32] !== t13) {
|
</Box>
|
||||||
t14 = <Text dimColor={true}>{elapsedTime}{t12}{t13}</Text>;
|
|
||||||
$[30] = elapsedTime;
|
{/* Error details if failed */}
|
||||||
$[31] = t12;
|
{teammate.status === 'failed' && teammate.error && (
|
||||||
$[32] = t13;
|
<Box flexDirection="column" marginTop={1}>
|
||||||
$[33] = t14;
|
<Text bold color="error">
|
||||||
} else {
|
Error
|
||||||
t14 = $[33];
|
</Text>
|
||||||
}
|
<Text color="error" wrap="wrap">
|
||||||
let t15;
|
{teammate.error}
|
||||||
if ($[34] !== t11 || $[35] !== t14) {
|
</Text>
|
||||||
t15 = <Text>{t11}{t14}</Text>;
|
</Box>
|
||||||
$[34] = t11;
|
)}
|
||||||
$[35] = t14;
|
</Dialog>
|
||||||
$[36] = t15;
|
</Box>
|
||||||
} else {
|
)
|
||||||
t15 = $[36];
|
|
||||||
}
|
|
||||||
const subtitle = t15;
|
|
||||||
let t16;
|
|
||||||
if ($[37] !== onBack || $[38] !== onForeground || $[39] !== onKill || $[40] !== teammate.status) {
|
|
||||||
t16 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{teammate.status === "running" && onKill && <KeyboardShortcutHint shortcut="x" action="stop" />}{teammate.status === "running" && onForeground && <KeyboardShortcutHint shortcut="f" action="foreground" />}</Byline>;
|
|
||||||
$[37] = onBack;
|
|
||||||
$[38] = onForeground;
|
|
||||||
$[39] = onKill;
|
|
||||||
$[40] = teammate.status;
|
|
||||||
$[41] = t16;
|
|
||||||
} else {
|
|
||||||
t16 = $[41];
|
|
||||||
}
|
|
||||||
let t17;
|
|
||||||
if ($[42] !== teammate.progress || $[43] !== teammate.status || $[44] !== theme) {
|
|
||||||
t17 = teammate.status === "running" && teammate.progress?.recentActivities && teammate.progress.recentActivities.length > 0 && <Box flexDirection="column"><Text bold={true} dimColor={true}>Progress</Text>{teammate.progress.recentActivities.map((activity_0, i) => <Text key={i} dimColor={i < teammate.progress.recentActivities.length - 1} wrap="truncate-end">{i === teammate.progress.recentActivities.length - 1 ? "\u203A " : " "}{renderToolActivity(activity_0, tools, theme)}</Text>)}</Box>;
|
|
||||||
$[42] = teammate.progress;
|
|
||||||
$[43] = teammate.status;
|
|
||||||
$[44] = theme;
|
|
||||||
$[45] = t17;
|
|
||||||
} else {
|
|
||||||
t17 = $[45];
|
|
||||||
}
|
|
||||||
let t18;
|
|
||||||
if ($[46] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t18 = <Text bold={true} dimColor={true}>Prompt</Text>;
|
|
||||||
$[46] = t18;
|
|
||||||
} else {
|
|
||||||
t18 = $[46];
|
|
||||||
}
|
|
||||||
let t19;
|
|
||||||
if ($[47] !== displayPrompt) {
|
|
||||||
t19 = <Box flexDirection="column" marginTop={1}>{t18}<Text wrap="wrap">{displayPrompt}</Text></Box>;
|
|
||||||
$[47] = displayPrompt;
|
|
||||||
$[48] = t19;
|
|
||||||
} else {
|
|
||||||
t19 = $[48];
|
|
||||||
}
|
|
||||||
let t20;
|
|
||||||
if ($[49] !== teammate.error || $[50] !== teammate.status) {
|
|
||||||
t20 = teammate.status === "failed" && teammate.error && <Box flexDirection="column" marginTop={1}><Text bold={true} color="error">Error</Text><Text color="error" wrap="wrap">{teammate.error}</Text></Box>;
|
|
||||||
$[49] = teammate.error;
|
|
||||||
$[50] = teammate.status;
|
|
||||||
$[51] = t20;
|
|
||||||
} else {
|
|
||||||
t20 = $[51];
|
|
||||||
}
|
|
||||||
let t21;
|
|
||||||
if ($[52] !== onDone || $[53] !== subtitle || $[54] !== t16 || $[55] !== t17 || $[56] !== t19 || $[57] !== t20 || $[58] !== title) {
|
|
||||||
t21 = <Dialog title={title} subtitle={subtitle} onCancel={onDone} color="background" inputGuide={t16}>{t17}{t19}{t20}</Dialog>;
|
|
||||||
$[52] = onDone;
|
|
||||||
$[53] = subtitle;
|
|
||||||
$[54] = t16;
|
|
||||||
$[55] = t17;
|
|
||||||
$[56] = t19;
|
|
||||||
$[57] = t20;
|
|
||||||
$[58] = title;
|
|
||||||
$[59] = t21;
|
|
||||||
} else {
|
|
||||||
t21 = $[59];
|
|
||||||
}
|
|
||||||
let t22;
|
|
||||||
if ($[60] !== handleKeyDown || $[61] !== t21) {
|
|
||||||
t22 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t21}</Box>;
|
|
||||||
$[60] = handleKeyDown;
|
|
||||||
$[61] = t21;
|
|
||||||
$[62] = t22;
|
|
||||||
} else {
|
|
||||||
t22 = $[62];
|
|
||||||
}
|
|
||||||
return t22;
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,17 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { useRef } from 'react'
|
||||||
import React, { useRef } from 'react';
|
import type { RemoteAgentTaskState } from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js'
|
||||||
import type { RemoteAgentTaskState } from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js';
|
import type { DeepImmutable } from 'src/types/utils.js'
|
||||||
import type { DeepImmutable } from 'src/types/utils.js';
|
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'
|
||||||
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js';
|
import { useSettings } from '../../hooks/useSettings.js'
|
||||||
import { useSettings } from '../../hooks/useSettings.js';
|
import { Text, useAnimationFrame } from '../../ink.js'
|
||||||
import { Text, useAnimationFrame } from '../../ink.js';
|
import { count } from '../../utils/array.js'
|
||||||
import { count } from '../../utils/array.js';
|
import { getRainbowColor } from '../../utils/thinking.js'
|
||||||
import { getRainbowColor } from '../../utils/thinking.js';
|
|
||||||
const TICK_MS = 80;
|
const TICK_MS = 80
|
||||||
type ReviewStage = NonNullable<NonNullable<RemoteAgentTaskState['reviewProgress']>['stage']>;
|
|
||||||
|
type ReviewStage = NonNullable<
|
||||||
|
NonNullable<RemoteAgentTaskState['reviewProgress']>['stage']
|
||||||
|
>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stage-appropriate counts line for a running review. Shared between the
|
* Stage-appropriate counts line for a running review. Shared between the
|
||||||
@@ -19,52 +22,48 @@ type ReviewStage = NonNullable<NonNullable<RemoteAgentTaskState['reviewProgress'
|
|||||||
* Canonical behavior: word labels (not ✓/✗), hide refuted when 0, "deduping"
|
* Canonical behavior: word labels (not ✓/✗), hide refuted when 0, "deduping"
|
||||||
* for the synthesizing stage (matches STAGE_LABELS in the detail dialog).
|
* for the synthesizing stage (matches STAGE_LABELS in the detail dialog).
|
||||||
*/
|
*/
|
||||||
export function formatReviewStageCounts(stage: ReviewStage | undefined, found: number, verified: number, refuted: number): string {
|
export function formatReviewStageCounts(
|
||||||
|
stage: ReviewStage | undefined,
|
||||||
|
found: number,
|
||||||
|
verified: number,
|
||||||
|
refuted: number,
|
||||||
|
): string {
|
||||||
// Pre-stage orchestrator images don't write the stage field.
|
// Pre-stage orchestrator images don't write the stage field.
|
||||||
if (!stage) return `${found} found · ${verified} verified`;
|
if (!stage) return `${found} found · ${verified} verified`
|
||||||
if (stage === 'synthesizing') {
|
if (stage === 'synthesizing') {
|
||||||
const parts = [`${verified} verified`];
|
const parts = [`${verified} verified`]
|
||||||
if (refuted > 0) parts.push(`${refuted} refuted`);
|
if (refuted > 0) parts.push(`${refuted} refuted`)
|
||||||
parts.push('deduping');
|
parts.push('deduping')
|
||||||
return parts.join(' · ');
|
return parts.join(' · ')
|
||||||
}
|
}
|
||||||
if (stage === 'verifying') {
|
if (stage === 'verifying') {
|
||||||
const parts = [`${found} found`, `${verified} verified`];
|
const parts = [`${found} found`, `${verified} verified`]
|
||||||
if (refuted > 0) parts.push(`${refuted} refuted`);
|
if (refuted > 0) parts.push(`${refuted} refuted`)
|
||||||
return parts.join(' · ');
|
return parts.join(' · ')
|
||||||
}
|
}
|
||||||
// stage === 'finding'
|
// stage === 'finding'
|
||||||
return found > 0 ? `${found} found` : 'finding';
|
return found > 0 ? `${found} found` : 'finding'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-character rainbow gradient, same treatment as the ultraplan keyword.
|
// Per-character rainbow gradient, same treatment as the ultraplan keyword.
|
||||||
// The phase offset lets the gradient cycle — so the colors sweep along the
|
// The phase offset lets the gradient cycle — so the colors sweep along the
|
||||||
// text on each animation frame instead of being static.
|
// text on each animation frame instead of being static.
|
||||||
function RainbowText(t0) {
|
function RainbowText({
|
||||||
const $ = _c(5);
|
text,
|
||||||
const {
|
phase = 0,
|
||||||
text,
|
}: {
|
||||||
phase: t1
|
text: string
|
||||||
} = t0;
|
phase?: number
|
||||||
const phase = t1 === undefined ? 0 : t1;
|
}): React.ReactNode {
|
||||||
let t2;
|
return (
|
||||||
if ($[0] !== text) {
|
<>
|
||||||
t2 = [...text];
|
{[...text].map((ch, i) => (
|
||||||
$[0] = text;
|
<Text key={i} color={getRainbowColor(i + phase)}>
|
||||||
$[1] = t2;
|
{ch}
|
||||||
} else {
|
</Text>
|
||||||
t2 = $[1];
|
))}
|
||||||
}
|
</>
|
||||||
let t3;
|
)
|
||||||
if ($[2] !== phase || $[3] !== t2) {
|
|
||||||
t3 = <>{t2.map((ch, i) => <Text key={i} color={getRainbowColor(i + phase)}>{ch}</Text>)}</>;
|
|
||||||
$[2] = phase;
|
|
||||||
$[3] = t2;
|
|
||||||
$[4] = t3;
|
|
||||||
} else {
|
|
||||||
t3 = $[4];
|
|
||||||
}
|
|
||||||
return t3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smooth-tick a count toward target, +1 per frame. Same pattern as the
|
// Smooth-tick a count toward target, +1 per frame. Same pattern as the
|
||||||
@@ -74,169 +73,129 @@ function RainbowText(t0) {
|
|||||||
// the clock is frozen), bypass the tick and jump straight to target —
|
// the clock is frozen), bypass the tick and jump straight to target —
|
||||||
// otherwise a frozen `time` would leave the ref stuck at its init value.
|
// otherwise a frozen `time` would leave the ref stuck at its init value.
|
||||||
function useSmoothCount(target: number, time: number, snap: boolean): number {
|
function useSmoothCount(target: number, time: number, snap: boolean): number {
|
||||||
const displayed = useRef(target);
|
const displayed = useRef(target)
|
||||||
const lastTick = useRef(time);
|
const lastTick = useRef(time)
|
||||||
if (snap || target < displayed.current) {
|
if (snap || target < displayed.current) {
|
||||||
displayed.current = target;
|
displayed.current = target
|
||||||
} else if (target > displayed.current && time !== lastTick.current) {
|
} else if (target > displayed.current && time !== lastTick.current) {
|
||||||
displayed.current += 1;
|
displayed.current += 1
|
||||||
lastTick.current = time;
|
lastTick.current = time
|
||||||
}
|
}
|
||||||
return displayed.current;
|
return displayed.current
|
||||||
}
|
}
|
||||||
function ReviewRainbowLine(t0) {
|
|
||||||
const $ = _c(15);
|
function ReviewRainbowLine({
|
||||||
const {
|
session,
|
||||||
session
|
}: {
|
||||||
} = t0;
|
session: DeepImmutable<RemoteAgentTaskState>
|
||||||
const settings = useSettings();
|
}): React.ReactNode {
|
||||||
const reducedMotion = settings.prefersReducedMotion ?? false;
|
const settings = useSettings()
|
||||||
const p = session.reviewProgress;
|
const reducedMotion = settings.prefersReducedMotion ?? false
|
||||||
const running = session.status === "running";
|
const p = session.reviewProgress
|
||||||
const [, time] = useAnimationFrame(running && !reducedMotion ? TICK_MS : null);
|
const running = session.status === 'running'
|
||||||
const targetFound = p?.bugsFound ?? 0;
|
// Animation clock runs only while running — completed/failed are static.
|
||||||
const targetVerified = p?.bugsVerified ?? 0;
|
// Disabled entirely when the user prefers reduced motion.
|
||||||
const targetRefuted = p?.bugsRefuted ?? 0;
|
//
|
||||||
const snap = reducedMotion || !running;
|
// The ref is intentionally discarded: this component is rendered inside
|
||||||
const found = useSmoothCount(targetFound, time, snap);
|
// <Text> wrappers (BackgroundTasksDialog, RemoteSessionDetailDialog), and
|
||||||
const verified = useSmoothCount(targetVerified, time, snap);
|
// Ink can't nest <Box> inside <Text>. Dropping the ref means
|
||||||
const refuted = useSmoothCount(targetRefuted, time, snap);
|
// useTerminalViewport's isVisible stays true, so the clock ticks even when
|
||||||
const phase = Math.floor(time / (TICK_MS * 3)) % 7;
|
// scrolled off-screen — acceptable for a single 30-char line.
|
||||||
if (session.status === "completed") {
|
const [, time] = useAnimationFrame(running && !reducedMotion ? TICK_MS : null)
|
||||||
let t1;
|
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
const targetFound = p?.bugsFound ?? 0
|
||||||
t1 = <><Text color="background">{DIAMOND_FILLED} </Text><RainbowText text="ultrareview" phase={0} /><Text dimColor={true}> ready · shift+↓ to view</Text></>;
|
const targetVerified = p?.bugsVerified ?? 0
|
||||||
$[0] = t1;
|
const targetRefuted = p?.bugsRefuted ?? 0
|
||||||
} else {
|
// snap when the clock isn't advancing (reduced motion, or not running) —
|
||||||
t1 = $[0];
|
// useAnimationFrame(null) freezes `time` at its mount value, which would
|
||||||
}
|
// leave the tick-gate permanently false.
|
||||||
return t1;
|
const snap = reducedMotion || !running
|
||||||
|
const found = useSmoothCount(targetFound, time, snap)
|
||||||
|
const verified = useSmoothCount(targetVerified, time, snap)
|
||||||
|
const refuted = useSmoothCount(targetRefuted, time, snap)
|
||||||
|
|
||||||
|
// Phase advances every 3 ticks so the gradient sweep is visible but
|
||||||
|
// not frantic. Modulo keeps it in the 7-color cycle.
|
||||||
|
const phase = Math.floor(time / (TICK_MS * 3)) % 7
|
||||||
|
|
||||||
|
// ◇ open diamond while running (teal, matches cloud-session accent), ◆
|
||||||
|
// filled when terminal. Rainbow is scoped to the word `ultrareview` only —
|
||||||
|
// per design feedback, "there is a limit to the glittering rainbow".
|
||||||
|
// Counts stay dimColor.
|
||||||
|
if (session.status === 'completed') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text color="background">{DIAMOND_FILLED} </Text>
|
||||||
|
<RainbowText text="ultrareview" phase={0} />
|
||||||
|
<Text dimColor> ready · shift+↓ to view</Text>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (session.status === "failed") {
|
if (session.status === 'failed') {
|
||||||
let t1;
|
return (
|
||||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
<>
|
||||||
t1 = <><Text color="background">{DIAMOND_FILLED} </Text><RainbowText text="ultrareview" phase={0} /><Text color="error" dimColor={true}>{" \xB7 "}error</Text></>;
|
<Text color="background">{DIAMOND_FILLED} </Text>
|
||||||
$[1] = t1;
|
<RainbowText text="ultrareview" phase={0} />
|
||||||
} else {
|
<Text color="error" dimColor>
|
||||||
t1 = $[1];
|
{' · '}
|
||||||
}
|
error
|
||||||
return t1;
|
</Text>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
let t1;
|
|
||||||
if ($[2] !== found || $[3] !== p || $[4] !== refuted || $[5] !== verified) {
|
// The !p branch ("setting up") covers the window before the orchestrator
|
||||||
t1 = !p ? "setting up" : formatReviewStageCounts(p.stage, found, verified, refuted);
|
// writes its first progress snapshot — container boot + repo clone can
|
||||||
$[2] = found;
|
// take 1-3 min, during which "0 found" looked hung.
|
||||||
$[3] = p;
|
const tail = !p
|
||||||
$[4] = refuted;
|
? 'setting up'
|
||||||
$[5] = verified;
|
: formatReviewStageCounts(p.stage, found, verified, refuted)
|
||||||
$[6] = t1;
|
return (
|
||||||
} else {
|
<>
|
||||||
t1 = $[6];
|
<Text color="background">{DIAMOND_OPEN} </Text>
|
||||||
}
|
<RainbowText text="ultrareview" phase={running ? phase : 0} />
|
||||||
const tail = t1;
|
<Text dimColor> · {tail}</Text>
|
||||||
let t2;
|
</>
|
||||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
)
|
||||||
t2 = <Text color="background">{DIAMOND_OPEN} </Text>;
|
|
||||||
$[7] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[7];
|
|
||||||
}
|
|
||||||
const t3 = running ? phase : 0;
|
|
||||||
let t4;
|
|
||||||
if ($[8] !== t3) {
|
|
||||||
t4 = <RainbowText text="ultrareview" phase={t3} />;
|
|
||||||
$[8] = t3;
|
|
||||||
$[9] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[9];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[10] !== tail) {
|
|
||||||
t5 = <Text dimColor={true}> · {tail}</Text>;
|
|
||||||
$[10] = tail;
|
|
||||||
$[11] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[11];
|
|
||||||
}
|
|
||||||
let t6;
|
|
||||||
if ($[12] !== t4 || $[13] !== t5) {
|
|
||||||
t6 = <>{t2}{t4}{t5}</>;
|
|
||||||
$[12] = t4;
|
|
||||||
$[13] = t5;
|
|
||||||
$[14] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[14];
|
|
||||||
}
|
|
||||||
return t6;
|
|
||||||
}
|
}
|
||||||
export function RemoteSessionProgress(t0) {
|
|
||||||
const $ = _c(11);
|
export function RemoteSessionProgress({
|
||||||
const {
|
session,
|
||||||
session
|
}: {
|
||||||
} = t0;
|
session: DeepImmutable<RemoteAgentTaskState>
|
||||||
|
}): React.ReactNode {
|
||||||
|
// Lite-review: rainbow gradient over the full line, ultraplan-style.
|
||||||
|
// BackgroundTask.tsx delegates the whole <Text> wrapper here so the
|
||||||
|
// gradient spans the title, not just the trailing status.
|
||||||
if (session.isRemoteReview) {
|
if (session.isRemoteReview) {
|
||||||
let t1;
|
return <ReviewRainbowLine session={session} />
|
||||||
if ($[0] !== session) {
|
|
||||||
t1 = <ReviewRainbowLine session={session} />;
|
|
||||||
$[0] = session;
|
|
||||||
$[1] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[1];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
}
|
||||||
if (session.status === "completed") {
|
|
||||||
let t1;
|
if (session.status === 'completed') {
|
||||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
return (
|
||||||
t1 = <Text bold={true} color="success" dimColor={true}>done</Text>;
|
<Text bold color="success" dimColor>
|
||||||
$[2] = t1;
|
done
|
||||||
} else {
|
</Text>
|
||||||
t1 = $[2];
|
)
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
}
|
||||||
if (session.status === "failed") {
|
|
||||||
let t1;
|
if (session.status === 'failed') {
|
||||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
return (
|
||||||
t1 = <Text bold={true} color="error" dimColor={true}>error</Text>;
|
<Text bold color="error" dimColor>
|
||||||
$[3] = t1;
|
error
|
||||||
} else {
|
</Text>
|
||||||
t1 = $[3];
|
)
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session.todoList.length) {
|
if (!session.todoList.length) {
|
||||||
let t1;
|
return <Text dimColor>{session.status}…</Text>
|
||||||
if ($[4] !== session.status) {
|
|
||||||
t1 = <Text dimColor={true}>{session.status}…</Text>;
|
|
||||||
$[4] = session.status;
|
|
||||||
$[5] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[5];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
}
|
||||||
let t1;
|
|
||||||
if ($[6] !== session.todoList) {
|
const completed = count(session.todoList, _ => _.status === 'completed')
|
||||||
t1 = count(session.todoList, _temp);
|
const total = session.todoList.length
|
||||||
$[6] = session.todoList;
|
return (
|
||||||
$[7] = t1;
|
<Text dimColor>
|
||||||
} else {
|
{completed}/{total}
|
||||||
t1 = $[7];
|
</Text>
|
||||||
}
|
)
|
||||||
const completed = t1;
|
|
||||||
const total = session.todoList.length;
|
|
||||||
let t2;
|
|
||||||
if ($[8] !== completed || $[9] !== total) {
|
|
||||||
t2 = <Text dimColor={true}>{completed}/{total}</Text>;
|
|
||||||
$[8] = completed;
|
|
||||||
$[9] = total;
|
|
||||||
$[10] = t2;
|
|
||||||
} else {
|
|
||||||
t2 = $[10];
|
|
||||||
}
|
|
||||||
return t2;
|
|
||||||
}
|
|
||||||
function _temp(_) {
|
|
||||||
return _.status === "completed";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,403 +1,247 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, {
|
||||||
import React, { Suspense, use, useDeferredValue, useEffect, useState } from 'react';
|
Suspense,
|
||||||
import type { DeepImmutable } from 'src/types/utils.js';
|
use,
|
||||||
import type { CommandResultDisplay } from '../../commands.js';
|
useDeferredValue,
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
useEffect,
|
||||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
useState,
|
||||||
import { Box, Text } from '../../ink.js';
|
} from 'react'
|
||||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
import type { DeepImmutable } from 'src/types/utils.js'
|
||||||
import type { LocalShellTaskState } from '../../tasks/LocalShellTask/guards.js';
|
import type { CommandResultDisplay } from '../../commands.js'
|
||||||
import { formatDuration, formatFileSize, truncateToWidth } from '../../utils/format.js';
|
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||||
import { tailFile } from '../../utils/fsOperations.js';
|
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||||
import { getTaskOutputPath } from '../../utils/task/diskOutput.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { Byline } from '../design-system/Byline.js';
|
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||||
import { Dialog } from '../design-system/Dialog.js';
|
import type { LocalShellTaskState } from '../../tasks/LocalShellTask/guards.js'
|
||||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
import {
|
||||||
|
formatDuration,
|
||||||
|
formatFileSize,
|
||||||
|
truncateToWidth,
|
||||||
|
} from '../../utils/format.js'
|
||||||
|
import { tailFile } from '../../utils/fsOperations.js'
|
||||||
|
import { getTaskOutputPath } from '../../utils/task/diskOutput.js'
|
||||||
|
import { Byline } from '../design-system/Byline.js'
|
||||||
|
import { Dialog } from '../design-system/Dialog.js'
|
||||||
|
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shell: DeepImmutable<LocalShellTaskState>;
|
shell: DeepImmutable<LocalShellTaskState>
|
||||||
onDone: (result?: string, options?: {
|
onDone: (
|
||||||
display?: CommandResultDisplay;
|
result?: string,
|
||||||
}) => void;
|
options?: { display?: CommandResultDisplay },
|
||||||
onKillShell?: () => void;
|
) => void
|
||||||
onBack?: () => void;
|
onKillShell?: () => void
|
||||||
};
|
onBack?: () => void
|
||||||
const SHELL_DETAIL_TAIL_BYTES = 8192;
|
}
|
||||||
|
|
||||||
|
const SHELL_DETAIL_TAIL_BYTES = 8192
|
||||||
|
|
||||||
type TaskOutputResult = {
|
type TaskOutputResult = {
|
||||||
content: string;
|
content: string
|
||||||
bytesTotal: number;
|
bytesTotal: number
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the tail of the task output file. Only reads the last few KB,
|
* Read the tail of the task output file. Only reads the last few KB,
|
||||||
* not the entire file.
|
* not the entire file.
|
||||||
*/
|
*/
|
||||||
async function getTaskOutput(shell: DeepImmutable<LocalShellTaskState>): Promise<TaskOutputResult> {
|
async function getTaskOutput(
|
||||||
const path = getTaskOutputPath(shell.id);
|
shell: DeepImmutable<LocalShellTaskState>,
|
||||||
|
): Promise<TaskOutputResult> {
|
||||||
|
const path = getTaskOutputPath(shell.id)
|
||||||
try {
|
try {
|
||||||
const result = await tailFile(path, SHELL_DETAIL_TAIL_BYTES);
|
const result = await tailFile(path, SHELL_DETAIL_TAIL_BYTES)
|
||||||
return {
|
return { content: result.content, bytesTotal: result.bytesTotal }
|
||||||
content: result.content,
|
|
||||||
bytesTotal: result.bytesTotal
|
|
||||||
};
|
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return { content: '', bytesTotal: 0 }
|
||||||
content: '',
|
|
||||||
bytesTotal: 0
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function ShellDetailDialog(t0) {
|
|
||||||
const $ = _c(57);
|
export function ShellDetailDialog({
|
||||||
const {
|
shell,
|
||||||
shell,
|
onDone,
|
||||||
onDone,
|
onKillShell,
|
||||||
onKillShell,
|
onBack,
|
||||||
onBack
|
}: Props): React.ReactNode {
|
||||||
} = t0;
|
const { columns } = useTerminalSize()
|
||||||
const {
|
|
||||||
columns
|
// Promise created in initializer (not during render). For running shells,
|
||||||
} = useTerminalSize();
|
// the effect timer replaces it periodically to pick up new output.
|
||||||
let t1;
|
// useDeferredValue keeps showing the previous output while the new promise
|
||||||
if ($[0] !== shell) {
|
// resolves, preventing the Suspense fallback from flickering.
|
||||||
t1 = () => getTaskOutput(shell);
|
const [outputPromise, setOutputPromise] = useState<Promise<TaskOutputResult>>(
|
||||||
$[0] = shell;
|
() => getTaskOutput(shell),
|
||||||
$[1] = t1;
|
)
|
||||||
} else {
|
const deferredOutputPromise = useDeferredValue(outputPromise)
|
||||||
t1 = $[1];
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shell.status !== 'running') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const timer = setInterval(
|
||||||
|
(setOutputPromise, shell) => setOutputPromise(getTaskOutput(shell)),
|
||||||
|
1000,
|
||||||
|
setOutputPromise,
|
||||||
|
shell,
|
||||||
|
)
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [shell.id, shell.status])
|
||||||
|
|
||||||
|
// Handle standard close action
|
||||||
|
const handleClose = () =>
|
||||||
|
onDone('Shell details dismissed', { display: 'system' })
|
||||||
|
|
||||||
|
// Handle additional close actions beyond Dialog's built-in Esc handler
|
||||||
|
useKeybindings(
|
||||||
|
{
|
||||||
|
'confirm:yes': handleClose,
|
||||||
|
},
|
||||||
|
{ context: 'Confirmation' },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle dialog-specific keys
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
onDone('Shell details dismissed', { display: 'system' })
|
||||||
|
} else if (e.key === 'left' && onBack) {
|
||||||
|
e.preventDefault()
|
||||||
|
onBack()
|
||||||
|
} else if (e.key === 'x' && shell.status === 'running' && onKillShell) {
|
||||||
|
e.preventDefault()
|
||||||
|
onKillShell()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const [outputPromise, setOutputPromise] = useState(t1);
|
|
||||||
const deferredOutputPromise = useDeferredValue(outputPromise);
|
// Truncate command if too long (for display purposes)
|
||||||
let t2;
|
const isMonitor = shell.kind === 'monitor'
|
||||||
if ($[2] !== shell) {
|
const displayCommand = truncateToWidth(shell.command, 280)
|
||||||
t2 = () => {
|
|
||||||
if (shell.status !== "running") {
|
return (
|
||||||
return;
|
<Box
|
||||||
}
|
flexDirection="column"
|
||||||
const timer = setInterval(_temp, 1000, setOutputPromise, shell);
|
tabIndex={0}
|
||||||
return () => clearInterval(timer);
|
autoFocus
|
||||||
};
|
onKeyDown={handleKeyDown}
|
||||||
$[2] = shell;
|
>
|
||||||
$[3] = t2;
|
<Dialog
|
||||||
} else {
|
title={isMonitor ? 'Monitor details' : 'Shell details'}
|
||||||
t2 = $[3];
|
onCancel={handleClose}
|
||||||
}
|
color="background"
|
||||||
let t3;
|
inputGuide={exitState =>
|
||||||
if ($[4] !== shell.id || $[5] !== shell.status) {
|
exitState.pending ? (
|
||||||
t3 = [shell.id, shell.status];
|
<Text>Press {exitState.keyName} again to exit</Text>
|
||||||
$[4] = shell.id;
|
) : (
|
||||||
$[5] = shell.status;
|
<Byline>
|
||||||
$[6] = t3;
|
{onBack && <KeyboardShortcutHint shortcut="←" action="go back" />}
|
||||||
} else {
|
<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />
|
||||||
t3 = $[6];
|
{shell.status === 'running' && onKillShell && (
|
||||||
}
|
<KeyboardShortcutHint shortcut="x" action="stop" />
|
||||||
useEffect(t2, t3);
|
)}
|
||||||
let t4;
|
</Byline>
|
||||||
if ($[7] !== onDone) {
|
)
|
||||||
t4 = () => onDone("Shell details dismissed", {
|
|
||||||
display: "system"
|
|
||||||
});
|
|
||||||
$[7] = onDone;
|
|
||||||
$[8] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[8];
|
|
||||||
}
|
|
||||||
const handleClose = t4;
|
|
||||||
let t5;
|
|
||||||
if ($[9] !== handleClose) {
|
|
||||||
t5 = {
|
|
||||||
"confirm:yes": handleClose
|
|
||||||
};
|
|
||||||
$[9] = handleClose;
|
|
||||||
$[10] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[10];
|
|
||||||
}
|
|
||||||
let t6;
|
|
||||||
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t6 = {
|
|
||||||
context: "Confirmation"
|
|
||||||
};
|
|
||||||
$[11] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[11];
|
|
||||||
}
|
|
||||||
useKeybindings(t5, t6);
|
|
||||||
let t7;
|
|
||||||
if ($[12] !== onBack || $[13] !== onDone || $[14] !== onKillShell || $[15] !== shell.status) {
|
|
||||||
t7 = e => {
|
|
||||||
if (e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
onDone("Shell details dismissed", {
|
|
||||||
display: "system"
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (e.key === "left" && onBack) {
|
|
||||||
e.preventDefault();
|
|
||||||
onBack();
|
|
||||||
} else {
|
|
||||||
if (e.key === "x" && shell.status === "running" && onKillShell) {
|
|
||||||
e.preventDefault();
|
|
||||||
onKillShell();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
>
|
||||||
};
|
<Box flexDirection="column">
|
||||||
$[12] = onBack;
|
<Text>
|
||||||
$[13] = onDone;
|
<Text bold>Status:</Text>{' '}
|
||||||
$[14] = onKillShell;
|
{shell.status === 'running' ? (
|
||||||
$[15] = shell.status;
|
<Text color="background">
|
||||||
$[16] = t7;
|
{shell.status}
|
||||||
} else {
|
{shell.result?.code !== undefined &&
|
||||||
t7 = $[16];
|
` (exit code: ${shell.result.code})`}
|
||||||
}
|
</Text>
|
||||||
const handleKeyDown = t7;
|
) : shell.status === 'completed' ? (
|
||||||
const isMonitor = shell.kind === "monitor";
|
<Text color="success">
|
||||||
let t8;
|
{shell.status}
|
||||||
if ($[17] !== shell.command) {
|
{shell.result?.code !== undefined &&
|
||||||
t8 = truncateToWidth(shell.command, 280);
|
` (exit code: ${shell.result.code})`}
|
||||||
$[17] = shell.command;
|
</Text>
|
||||||
$[18] = t8;
|
) : (
|
||||||
} else {
|
<Text color="error">
|
||||||
t8 = $[18];
|
{shell.status}
|
||||||
}
|
{shell.result?.code !== undefined &&
|
||||||
const displayCommand = t8;
|
` (exit code: ${shell.result.code})`}
|
||||||
const t9 = isMonitor ? "Monitor details" : "Shell details";
|
</Text>
|
||||||
let t10;
|
)}
|
||||||
if ($[19] !== onBack || $[20] !== onKillShell || $[21] !== shell.status) {
|
</Text>
|
||||||
t10 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{shell.status === "running" && onKillShell && <KeyboardShortcutHint shortcut="x" action="stop" />}</Byline>;
|
<Text>
|
||||||
$[19] = onBack;
|
<Text bold>Runtime:</Text>{' '}
|
||||||
$[20] = onKillShell;
|
{formatDuration((shell.endTime ?? Date.now()) - shell.startTime)}
|
||||||
$[21] = shell.status;
|
</Text>
|
||||||
$[22] = t10;
|
<Text wrap="wrap">
|
||||||
} else {
|
<Text bold>{isMonitor ? 'Script:' : 'Command:'}</Text>{' '}
|
||||||
t10 = $[22];
|
{displayCommand}
|
||||||
}
|
</Text>
|
||||||
let t11;
|
</Box>
|
||||||
if ($[23] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t11 = <Text bold={true}>Status:</Text>;
|
<Box flexDirection="column">
|
||||||
$[23] = t11;
|
<Text bold>Output:</Text>
|
||||||
} else {
|
<Suspense fallback={<Text dimColor>Loading output…</Text>}>
|
||||||
t11 = $[23];
|
<ShellOutputContent
|
||||||
}
|
outputPromise={deferredOutputPromise}
|
||||||
let t12;
|
columns={columns}
|
||||||
if ($[24] !== shell.result || $[25] !== shell.status) {
|
/>
|
||||||
t12 = <Text>{t11}{" "}{shell.status === "running" ? <Text color="background">{shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`}</Text> : shell.status === "completed" ? <Text color="success">{shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`}</Text> : <Text color="error">{shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`}</Text>}</Text>;
|
</Suspense>
|
||||||
$[24] = shell.result;
|
</Box>
|
||||||
$[25] = shell.status;
|
</Dialog>
|
||||||
$[26] = t12;
|
</Box>
|
||||||
} else {
|
)
|
||||||
t12 = $[26];
|
|
||||||
}
|
|
||||||
let t13;
|
|
||||||
if ($[27] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t13 = <Text bold={true}>Runtime:</Text>;
|
|
||||||
$[27] = t13;
|
|
||||||
} else {
|
|
||||||
t13 = $[27];
|
|
||||||
}
|
|
||||||
let t14;
|
|
||||||
if ($[28] !== shell.endTime) {
|
|
||||||
t14 = shell.endTime ?? Date.now();
|
|
||||||
$[28] = shell.endTime;
|
|
||||||
$[29] = t14;
|
|
||||||
} else {
|
|
||||||
t14 = $[29];
|
|
||||||
}
|
|
||||||
const t15 = t14 - shell.startTime;
|
|
||||||
let t16;
|
|
||||||
if ($[30] !== t15) {
|
|
||||||
t16 = formatDuration(t15);
|
|
||||||
$[30] = t15;
|
|
||||||
$[31] = t16;
|
|
||||||
} else {
|
|
||||||
t16 = $[31];
|
|
||||||
}
|
|
||||||
let t17;
|
|
||||||
if ($[32] !== t16) {
|
|
||||||
t17 = <Text>{t13}{" "}{t16}</Text>;
|
|
||||||
$[32] = t16;
|
|
||||||
$[33] = t17;
|
|
||||||
} else {
|
|
||||||
t17 = $[33];
|
|
||||||
}
|
|
||||||
const t18 = isMonitor ? "Script:" : "Command:";
|
|
||||||
let t19;
|
|
||||||
if ($[34] !== t18) {
|
|
||||||
t19 = <Text bold={true}>{t18}</Text>;
|
|
||||||
$[34] = t18;
|
|
||||||
$[35] = t19;
|
|
||||||
} else {
|
|
||||||
t19 = $[35];
|
|
||||||
}
|
|
||||||
let t20;
|
|
||||||
if ($[36] !== displayCommand || $[37] !== t19) {
|
|
||||||
t20 = <Text wrap="wrap">{t19}{" "}{displayCommand}</Text>;
|
|
||||||
$[36] = displayCommand;
|
|
||||||
$[37] = t19;
|
|
||||||
$[38] = t20;
|
|
||||||
} else {
|
|
||||||
t20 = $[38];
|
|
||||||
}
|
|
||||||
let t21;
|
|
||||||
if ($[39] !== t12 || $[40] !== t17 || $[41] !== t20) {
|
|
||||||
t21 = <Box flexDirection="column">{t12}{t17}{t20}</Box>;
|
|
||||||
$[39] = t12;
|
|
||||||
$[40] = t17;
|
|
||||||
$[41] = t20;
|
|
||||||
$[42] = t21;
|
|
||||||
} else {
|
|
||||||
t21 = $[42];
|
|
||||||
}
|
|
||||||
let t22;
|
|
||||||
if ($[43] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t22 = <Text bold={true}>Output:</Text>;
|
|
||||||
$[43] = t22;
|
|
||||||
} else {
|
|
||||||
t22 = $[43];
|
|
||||||
}
|
|
||||||
let t23;
|
|
||||||
if ($[44] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t23 = <Text dimColor={true}>Loading output…</Text>;
|
|
||||||
$[44] = t23;
|
|
||||||
} else {
|
|
||||||
t23 = $[44];
|
|
||||||
}
|
|
||||||
let t24;
|
|
||||||
if ($[45] !== columns || $[46] !== deferredOutputPromise) {
|
|
||||||
t24 = <Box flexDirection="column">{t22}<Suspense fallback={t23}><ShellOutputContent outputPromise={deferredOutputPromise} columns={columns} /></Suspense></Box>;
|
|
||||||
$[45] = columns;
|
|
||||||
$[46] = deferredOutputPromise;
|
|
||||||
$[47] = t24;
|
|
||||||
} else {
|
|
||||||
t24 = $[47];
|
|
||||||
}
|
|
||||||
let t25;
|
|
||||||
if ($[48] !== handleClose || $[49] !== t10 || $[50] !== t21 || $[51] !== t24 || $[52] !== t9) {
|
|
||||||
t25 = <Dialog title={t9} onCancel={handleClose} color="background" inputGuide={t10}>{t21}{t24}</Dialog>;
|
|
||||||
$[48] = handleClose;
|
|
||||||
$[49] = t10;
|
|
||||||
$[50] = t21;
|
|
||||||
$[51] = t24;
|
|
||||||
$[52] = t9;
|
|
||||||
$[53] = t25;
|
|
||||||
} else {
|
|
||||||
t25 = $[53];
|
|
||||||
}
|
|
||||||
let t26;
|
|
||||||
if ($[54] !== handleKeyDown || $[55] !== t25) {
|
|
||||||
t26 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t25}</Box>;
|
|
||||||
$[54] = handleKeyDown;
|
|
||||||
$[55] = t25;
|
|
||||||
$[56] = t26;
|
|
||||||
} else {
|
|
||||||
t26 = $[56];
|
|
||||||
}
|
|
||||||
return t26;
|
|
||||||
}
|
|
||||||
function _temp(setOutputPromise_0, shell_0) {
|
|
||||||
return setOutputPromise_0(getTaskOutput(shell_0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShellOutputContentProps = {
|
type ShellOutputContentProps = {
|
||||||
outputPromise: Promise<TaskOutputResult>;
|
outputPromise: Promise<TaskOutputResult>
|
||||||
columns: number;
|
columns: number
|
||||||
};
|
}
|
||||||
function ShellOutputContent(t0) {
|
|
||||||
const $ = _c(19);
|
function ShellOutputContent({
|
||||||
const {
|
outputPromise,
|
||||||
outputPromise,
|
columns,
|
||||||
columns
|
}: ShellOutputContentProps): React.ReactNode {
|
||||||
} = t0;
|
const { content, bytesTotal } = use(outputPromise)
|
||||||
const {
|
|
||||||
content,
|
|
||||||
bytesTotal
|
|
||||||
} = use(outputPromise) as any;
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
let t1;
|
return <Text dimColor>No output available</Text>
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t1 = <Text dimColor={true}>No output available</Text>;
|
|
||||||
$[0] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[0];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
}
|
||||||
let isIncomplete;
|
|
||||||
let rendered;
|
// Find last 10 line boundaries via lastIndexOf
|
||||||
if ($[1] !== bytesTotal || $[2] !== content) {
|
const starts: number[] = []
|
||||||
const starts = [];
|
let pos = content.length
|
||||||
let pos = content.length;
|
for (let i = 0; i < 10 && pos > 0; i++) {
|
||||||
for (let i = 0; i < 10 && pos > 0; i++) {
|
const prev = content.lastIndexOf('\n', pos - 1)
|
||||||
const prev = content.lastIndexOf("\n", pos - 1);
|
starts.push(prev + 1)
|
||||||
starts.push(prev + 1);
|
pos = prev
|
||||||
pos = prev;
|
|
||||||
}
|
|
||||||
starts.reverse();
|
|
||||||
isIncomplete = bytesTotal > content.length;
|
|
||||||
rendered = [];
|
|
||||||
for (let i_0 = 0; i_0 < starts.length; i_0++) {
|
|
||||||
const start = starts[i_0];
|
|
||||||
const end = i_0 < starts.length - 1 ? starts[i_0 + 1] - 1 : content.length;
|
|
||||||
const line = content.slice(start, end);
|
|
||||||
if (line) {
|
|
||||||
rendered.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$[1] = bytesTotal;
|
|
||||||
$[2] = content;
|
|
||||||
$[3] = isIncomplete;
|
|
||||||
$[4] = rendered;
|
|
||||||
} else {
|
|
||||||
isIncomplete = $[3];
|
|
||||||
rendered = $[4];
|
|
||||||
}
|
}
|
||||||
const t1 = columns - 6;
|
starts.reverse()
|
||||||
let t2;
|
const isIncomplete = bytesTotal > content.length
|
||||||
if ($[5] !== rendered) {
|
|
||||||
t2 = rendered.map(_temp2);
|
// Build lines, skip empty trailing/leading segments
|
||||||
$[5] = rendered;
|
const rendered: string[] = []
|
||||||
$[6] = t2;
|
for (let i = 0; i < starts.length; i++) {
|
||||||
} else {
|
const start = starts[i]!
|
||||||
t2 = $[6];
|
const end = i < starts.length - 1 ? starts[i + 1]! - 1 : content.length
|
||||||
|
const line = content.slice(start, end)
|
||||||
|
if (line) rendered.push(line)
|
||||||
}
|
}
|
||||||
let t3;
|
|
||||||
if ($[7] !== t1 || $[8] !== t2) {
|
return (
|
||||||
t3 = <Box borderStyle="round" paddingX={1} flexDirection="column" height={12} maxWidth={t1}>{t2}</Box>;
|
<>
|
||||||
$[7] = t1;
|
<Box
|
||||||
$[8] = t2;
|
borderStyle="round"
|
||||||
$[9] = t3;
|
paddingX={1}
|
||||||
} else {
|
flexDirection="column"
|
||||||
t3 = $[9];
|
height={12}
|
||||||
}
|
maxWidth={columns - 6}
|
||||||
const t4 = `Showing ${rendered.length} lines`;
|
>
|
||||||
let t5;
|
{rendered.map((line, i) => (
|
||||||
if ($[10] !== bytesTotal || $[11] !== isIncomplete) {
|
<Text key={i} wrap="truncate-end">
|
||||||
t5 = isIncomplete ? ` of ${formatFileSize(bytesTotal)}` : "";
|
{line}
|
||||||
$[10] = bytesTotal;
|
</Text>
|
||||||
$[11] = isIncomplete;
|
))}
|
||||||
$[12] = t5;
|
</Box>
|
||||||
} else {
|
<Text dimColor italic>
|
||||||
t5 = $[12];
|
{`Showing ${rendered.length} lines`}
|
||||||
}
|
{isIncomplete ? ` of ${formatFileSize(bytesTotal)}` : ''}
|
||||||
let t6;
|
</Text>
|
||||||
if ($[13] !== t4 || $[14] !== t5) {
|
</>
|
||||||
t6 = <Text dimColor={true} italic={true}>{t4}{t5}</Text>;
|
)
|
||||||
$[13] = t4;
|
|
||||||
$[14] = t5;
|
|
||||||
$[15] = t6;
|
|
||||||
} else {
|
|
||||||
t6 = $[15];
|
|
||||||
}
|
|
||||||
let t7;
|
|
||||||
if ($[16] !== t3 || $[17] !== t6) {
|
|
||||||
t7 = <>{t3}{t6}</>;
|
|
||||||
$[16] = t3;
|
|
||||||
$[17] = t6;
|
|
||||||
$[18] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[18];
|
|
||||||
}
|
|
||||||
return t7;
|
|
||||||
}
|
|
||||||
function _temp2(line_0, i_1) {
|
|
||||||
return <Text key={i_1} wrap="truncate-end">{line_0}</Text>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +1,52 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import type { ReactNode } from 'react'
|
||||||
import type { ReactNode } from 'react';
|
import React from 'react'
|
||||||
import React from 'react';
|
import { Text } from 'src/ink.js'
|
||||||
import { Text } from 'src/ink.js';
|
import type { TaskStatus } from 'src/Task.js'
|
||||||
import type { TaskStatus } from 'src/Task.js';
|
import type { LocalShellTaskState } from 'src/tasks/LocalShellTask/guards.js'
|
||||||
import type { LocalShellTaskState } from 'src/tasks/LocalShellTask/guards.js';
|
import type { DeepImmutable } from 'src/types/utils.js'
|
||||||
import type { DeepImmutable } from 'src/types/utils.js';
|
|
||||||
type TaskStatusTextProps = {
|
type TaskStatusTextProps = {
|
||||||
status: TaskStatus;
|
status: TaskStatus
|
||||||
label?: string;
|
label?: string
|
||||||
suffix?: string;
|
suffix?: string
|
||||||
};
|
|
||||||
export function TaskStatusText(t0) {
|
|
||||||
const $ = _c(4);
|
|
||||||
const {
|
|
||||||
status,
|
|
||||||
label,
|
|
||||||
suffix
|
|
||||||
} = t0;
|
|
||||||
const displayLabel = label ?? status;
|
|
||||||
const color = status === "completed" ? "success" : status === "failed" ? "error" : status === "killed" ? "warning" : undefined;
|
|
||||||
let t1;
|
|
||||||
if ($[0] !== color || $[1] !== displayLabel || $[2] !== suffix) {
|
|
||||||
t1 = <Text color={color} dimColor={true}>({displayLabel}{suffix})</Text>;
|
|
||||||
$[0] = color;
|
|
||||||
$[1] = displayLabel;
|
|
||||||
$[2] = suffix;
|
|
||||||
$[3] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[3];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
}
|
||||||
export function ShellProgress(t0) {
|
|
||||||
const $ = _c(4);
|
export function TaskStatusText({
|
||||||
const {
|
status,
|
||||||
shell
|
label,
|
||||||
} = t0;
|
suffix,
|
||||||
|
}: TaskStatusTextProps): ReactNode {
|
||||||
|
const displayLabel = label ?? status
|
||||||
|
const color =
|
||||||
|
status === 'completed'
|
||||||
|
? 'success'
|
||||||
|
: status === 'failed'
|
||||||
|
? 'error'
|
||||||
|
: status === 'killed'
|
||||||
|
? 'warning'
|
||||||
|
: undefined
|
||||||
|
return (
|
||||||
|
<Text color={color} dimColor>
|
||||||
|
({displayLabel}
|
||||||
|
{suffix})
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShellProgress({
|
||||||
|
shell,
|
||||||
|
}: {
|
||||||
|
shell: DeepImmutable<LocalShellTaskState>
|
||||||
|
}): ReactNode {
|
||||||
switch (shell.status) {
|
switch (shell.status) {
|
||||||
case "completed":
|
case 'completed':
|
||||||
{
|
return <TaskStatusText status="completed" label="done" />
|
||||||
let t1;
|
case 'failed':
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
return <TaskStatusText status="failed" label="error" />
|
||||||
t1 = <TaskStatusText status="completed" label="done" />;
|
case 'killed':
|
||||||
$[0] = t1;
|
return <TaskStatusText status="killed" label="stopped" />
|
||||||
} else {
|
case 'running':
|
||||||
t1 = $[0];
|
case 'pending':
|
||||||
}
|
return <TaskStatusText status="running" />
|
||||||
return t1;
|
|
||||||
}
|
|
||||||
case "failed":
|
|
||||||
{
|
|
||||||
let t1;
|
|
||||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t1 = <TaskStatusText status="failed" label="error" />;
|
|
||||||
$[1] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[1];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
|
||||||
case "killed":
|
|
||||||
{
|
|
||||||
let t1;
|
|
||||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t1 = <TaskStatusText status="killed" label="stopped" />;
|
|
||||||
$[2] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[2];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
|
||||||
case "running":
|
|
||||||
case "pending":
|
|
||||||
{
|
|
||||||
let t1;
|
|
||||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t1 = <TaskStatusText status="running" />;
|
|
||||||
$[3] = t1;
|
|
||||||
} else {
|
|
||||||
t1 = $[3];
|
|
||||||
}
|
|
||||||
return t1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,39 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { Text } from '../../ink.js';
|
import { Text } from '../../ink.js'
|
||||||
import type { Tools } from '../../Tool.js';
|
import type { Tools } from '../../Tool.js'
|
||||||
import { findToolByName } from '../../Tool.js';
|
import { findToolByName } from '../../Tool.js'
|
||||||
import type { ToolActivity } from '../../tasks/LocalAgentTask/LocalAgentTask.js';
|
import type { ToolActivity } from '../../tasks/LocalAgentTask/LocalAgentTask.js'
|
||||||
import type { ThemeName } from '../../utils/theme.js';
|
import type { ThemeName } from '../../utils/theme.js'
|
||||||
export function renderToolActivity(activity: ToolActivity, tools: Tools, theme: ThemeName): React.ReactNode {
|
|
||||||
const tool = findToolByName(tools, activity.toolName);
|
export function renderToolActivity(
|
||||||
|
activity: ToolActivity,
|
||||||
|
tools: Tools,
|
||||||
|
theme: ThemeName,
|
||||||
|
): React.ReactNode {
|
||||||
|
const tool = findToolByName(tools, activity.toolName)
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
return activity.toolName;
|
return activity.toolName
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const parsed = tool.inputSchema.safeParse(activity.input);
|
const parsed = tool.inputSchema.safeParse(activity.input)
|
||||||
const parsedInput = parsed.success ? parsed.data : {};
|
const parsedInput = parsed.success ? parsed.data : {}
|
||||||
const userFacingName = tool.userFacingName(parsedInput);
|
const userFacingName = tool.userFacingName(parsedInput)
|
||||||
if (!userFacingName) {
|
if (!userFacingName) {
|
||||||
return activity.toolName;
|
return activity.toolName
|
||||||
}
|
}
|
||||||
const toolArgs = tool.renderToolUseMessage(parsedInput, {
|
const toolArgs = tool.renderToolUseMessage(parsedInput, {
|
||||||
theme,
|
theme,
|
||||||
verbose: false
|
verbose: false,
|
||||||
});
|
})
|
||||||
if (toolArgs) {
|
if (toolArgs) {
|
||||||
return <Text>
|
return (
|
||||||
|
<Text>
|
||||||
{userFacingName}({toolArgs})
|
{userFacingName}({toolArgs})
|
||||||
</Text>;
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return userFacingName;
|
return userFacingName
|
||||||
} catch {
|
} catch {
|
||||||
return activity.toolName;
|
return activity.toolName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,71 +2,73 @@
|
|||||||
* Shared utilities for displaying task status across different task types.
|
* Shared utilities for displaying task status across different task types.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import figures from 'figures';
|
import figures from 'figures'
|
||||||
import type { TaskStatus } from 'src/Task.js';
|
import type { TaskStatus } from 'src/Task.js'
|
||||||
import type { InProcessTeammateTaskState } from 'src/tasks/InProcessTeammateTask/types.js';
|
import type { InProcessTeammateTaskState } from 'src/tasks/InProcessTeammateTask/types.js'
|
||||||
import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js';
|
import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
|
||||||
import { isBackgroundTask, type TaskState } from 'src/tasks/types.js';
|
import { isBackgroundTask, type TaskState } from 'src/tasks/types.js'
|
||||||
import type { DeepImmutable } from 'src/types/utils.js';
|
import type { DeepImmutable } from 'src/types/utils.js'
|
||||||
import { summarizeRecentActivities } from 'src/utils/collapseReadSearch.js';
|
import { summarizeRecentActivities } from 'src/utils/collapseReadSearch.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the given task status represents a terminal (finished) state.
|
* Returns true if the given task status represents a terminal (finished) state.
|
||||||
*/
|
*/
|
||||||
export function isTerminalStatus(status: TaskStatus): boolean {
|
export function isTerminalStatus(status: TaskStatus): boolean {
|
||||||
return status === 'completed' || status === 'failed' || status === 'killed';
|
return status === 'completed' || status === 'failed' || status === 'killed'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the appropriate icon for a task based on status and state flags.
|
* Returns the appropriate icon for a task based on status and state flags.
|
||||||
*/
|
*/
|
||||||
export function getTaskStatusIcon(status: TaskStatus, options?: {
|
export function getTaskStatusIcon(
|
||||||
isIdle?: boolean;
|
status: TaskStatus,
|
||||||
awaitingApproval?: boolean;
|
options?: {
|
||||||
hasError?: boolean;
|
isIdle?: boolean
|
||||||
shutdownRequested?: boolean;
|
awaitingApproval?: boolean
|
||||||
}): string {
|
hasError?: boolean
|
||||||
const {
|
shutdownRequested?: boolean
|
||||||
isIdle,
|
},
|
||||||
awaitingApproval,
|
): string {
|
||||||
hasError,
|
const { isIdle, awaitingApproval, hasError, shutdownRequested } =
|
||||||
shutdownRequested
|
options ?? {}
|
||||||
} = options ?? {};
|
|
||||||
if (hasError) return figures.cross;
|
if (hasError) return figures.cross
|
||||||
if (awaitingApproval) return figures.questionMarkPrefix;
|
if (awaitingApproval) return figures.questionMarkPrefix
|
||||||
if (shutdownRequested) return figures.warning;
|
if (shutdownRequested) return figures.warning
|
||||||
|
|
||||||
if (status === 'running') {
|
if (status === 'running') {
|
||||||
if (isIdle) return figures.ellipsis;
|
if (isIdle) return figures.ellipsis
|
||||||
return figures.play;
|
return figures.play
|
||||||
}
|
}
|
||||||
if (status === 'completed') return figures.tick;
|
if (status === 'completed') return figures.tick
|
||||||
if (status === 'failed' || status === 'killed') return figures.cross;
|
if (status === 'failed' || status === 'killed') return figures.cross
|
||||||
return figures.bullet;
|
return figures.bullet
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the appropriate semantic color for a task based on status and state flags.
|
* Returns the appropriate semantic color for a task based on status and state flags.
|
||||||
*/
|
*/
|
||||||
export function getTaskStatusColor(status: TaskStatus, options?: {
|
export function getTaskStatusColor(
|
||||||
isIdle?: boolean;
|
status: TaskStatus,
|
||||||
awaitingApproval?: boolean;
|
options?: {
|
||||||
hasError?: boolean;
|
isIdle?: boolean
|
||||||
shutdownRequested?: boolean;
|
awaitingApproval?: boolean
|
||||||
}): 'success' | 'error' | 'warning' | 'background' {
|
hasError?: boolean
|
||||||
const {
|
shutdownRequested?: boolean
|
||||||
isIdle,
|
},
|
||||||
awaitingApproval,
|
): 'success' | 'error' | 'warning' | 'background' {
|
||||||
hasError,
|
const { isIdle, awaitingApproval, hasError, shutdownRequested } =
|
||||||
shutdownRequested
|
options ?? {}
|
||||||
} = options ?? {};
|
|
||||||
if (hasError) return 'error';
|
if (hasError) return 'error'
|
||||||
if (awaitingApproval) return 'warning';
|
if (awaitingApproval) return 'warning'
|
||||||
if (shutdownRequested) return 'warning';
|
if (shutdownRequested) return 'warning'
|
||||||
if (isIdle) return 'background';
|
if (isIdle) return 'background'
|
||||||
if (status === 'completed') return 'success';
|
|
||||||
if (status === 'failed') return 'error';
|
if (status === 'completed') return 'success'
|
||||||
if (status === 'killed') return 'warning';
|
if (status === 'failed') return 'error'
|
||||||
return 'background';
|
if (status === 'killed') return 'warning'
|
||||||
|
return 'background'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,11 +76,18 @@ export function getTaskStatusColor(status: TaskStatus, options?: {
|
|||||||
* accounting for shutdown/approval/idle states and falling back through
|
* accounting for shutdown/approval/idle states and falling back through
|
||||||
* recent-activity summary → last activity description → 'working'.
|
* recent-activity summary → last activity description → 'working'.
|
||||||
*/
|
*/
|
||||||
export function describeTeammateActivity(t: DeepImmutable<InProcessTeammateTaskState>): string {
|
export function describeTeammateActivity(
|
||||||
if (t.shutdownRequested) return 'stopping';
|
t: DeepImmutable<InProcessTeammateTaskState>,
|
||||||
if (t.awaitingPlanApproval) return 'awaiting approval';
|
): string {
|
||||||
if (t.isIdle) return 'idle';
|
if (t.shutdownRequested) return 'stopping'
|
||||||
return (t.progress?.recentActivities && summarizeRecentActivities(t.progress.recentActivities)) ?? t.progress?.lastActivity?.activityDescription ?? 'working';
|
if (t.awaitingPlanApproval) return 'awaiting approval'
|
||||||
|
if (t.isIdle) return 'idle'
|
||||||
|
return (
|
||||||
|
(t.progress?.recentActivities &&
|
||||||
|
summarizeRecentActivities(t.progress.recentActivities)) ??
|
||||||
|
t.progress?.lastActivity?.activityDescription ??
|
||||||
|
'working'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,17 +99,21 @@ export function describeTeammateActivity(t: DeepImmutable<InProcessTeammateTaskS
|
|||||||
* plus exclusion of panel-managed agent tasks for ants (those are shown
|
* plus exclusion of panel-managed agent tasks for ants (those are shown
|
||||||
* by CoordinatorTaskPanel).
|
* by CoordinatorTaskPanel).
|
||||||
*/
|
*/
|
||||||
export function shouldHideTasksFooter(tasks: {
|
export function shouldHideTasksFooter(
|
||||||
[taskId: string]: TaskState;
|
tasks: { [taskId: string]: TaskState },
|
||||||
}, showSpinnerTree: boolean): boolean {
|
showSpinnerTree: boolean,
|
||||||
if (!showSpinnerTree) return false;
|
): boolean {
|
||||||
let hasVisibleTask = false;
|
if (!showSpinnerTree) return false
|
||||||
|
let hasVisibleTask = false
|
||||||
for (const t of Object.values(tasks) as TaskState[]) {
|
for (const t of Object.values(tasks) as TaskState[]) {
|
||||||
if (!isBackgroundTask(t) || (process.env.USER_TYPE) === 'ant' && isPanelAgentTask(t)) {
|
if (
|
||||||
continue;
|
!isBackgroundTask(t) ||
|
||||||
|
(process.env.USER_TYPE === 'ant' && isPanelAgentTask(t))
|
||||||
|
) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
hasVisibleTask = true;
|
hasVisibleTask = true
|
||||||
if (t.type !== 'in_process_teammate') return false;
|
if (t.type !== 'in_process_teammate') return false
|
||||||
}
|
}
|
||||||
return hasVisibleTask;
|
return hasVisibleTask
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,48 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, { type ReactNode } from 'react'
|
||||||
import React, { type ReactNode } from 'react';
|
import type { Theme } from '../../utils/theme.js'
|
||||||
import type { Theme } from '../../utils/theme.js';
|
import { Dialog } from '../design-system/Dialog.js'
|
||||||
import { Dialog } from '../design-system/Dialog.js';
|
import { useWizard } from './useWizard.js'
|
||||||
import { useWizard } from './useWizard.js';
|
import { WizardNavigationFooter } from './WizardNavigationFooter.js'
|
||||||
import { WizardNavigationFooter } from './WizardNavigationFooter.js';
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title?: string;
|
title?: string
|
||||||
color?: keyof Theme;
|
color?: keyof Theme
|
||||||
children: ReactNode;
|
children: ReactNode
|
||||||
subtitle?: string;
|
subtitle?: string
|
||||||
footerText?: ReactNode;
|
footerText?: ReactNode
|
||||||
};
|
}
|
||||||
export function WizardDialogLayout(t0) {
|
|
||||||
const $ = _c(11);
|
export function WizardDialogLayout({
|
||||||
const {
|
title: titleOverride,
|
||||||
title: titleOverride,
|
color = 'suggestion',
|
||||||
color: t1,
|
children,
|
||||||
children,
|
subtitle,
|
||||||
subtitle,
|
footerText,
|
||||||
footerText
|
}: Props): ReactNode {
|
||||||
} = t0;
|
|
||||||
const color = t1 === undefined ? "suggestion" : t1;
|
|
||||||
const {
|
const {
|
||||||
currentStepIndex,
|
currentStepIndex,
|
||||||
totalSteps,
|
totalSteps,
|
||||||
title: providerTitle,
|
title: providerTitle,
|
||||||
showStepCounter,
|
showStepCounter,
|
||||||
goBack
|
goBack,
|
||||||
} = useWizard();
|
} = useWizard()
|
||||||
const title = titleOverride || providerTitle || "Wizard";
|
const title = titleOverride || providerTitle || 'Wizard'
|
||||||
const stepSuffix = showStepCounter !== false ? ` (${currentStepIndex + 1}/${totalSteps})` : "";
|
const stepSuffix =
|
||||||
const t2 = `${title}${stepSuffix}`;
|
showStepCounter !== false ? ` (${currentStepIndex + 1}/${totalSteps})` : ''
|
||||||
let t3;
|
|
||||||
if ($[0] !== children || $[1] !== color || $[2] !== goBack || $[3] !== subtitle || $[4] !== t2) {
|
return (
|
||||||
t3 = <Dialog title={t2} subtitle={subtitle} onCancel={goBack} color={color} hideInputGuide={true} isCancelActive={false}>{children}</Dialog>;
|
<>
|
||||||
$[0] = children;
|
<Dialog
|
||||||
$[1] = color;
|
title={`${title}${stepSuffix}`}
|
||||||
$[2] = goBack;
|
subtitle={subtitle}
|
||||||
$[3] = subtitle;
|
onCancel={goBack}
|
||||||
$[4] = t2;
|
color={color}
|
||||||
$[5] = t3;
|
hideInputGuide
|
||||||
} else {
|
isCancelActive={false}
|
||||||
t3 = $[5];
|
>
|
||||||
}
|
{children}
|
||||||
let t4;
|
</Dialog>
|
||||||
if ($[6] !== footerText) {
|
<WizardNavigationFooter instructions={footerText} />
|
||||||
t4 = <WizardNavigationFooter instructions={footerText} />;
|
</>
|
||||||
$[6] = footerText;
|
)
|
||||||
$[7] = t4;
|
|
||||||
} else {
|
|
||||||
t4 = $[7];
|
|
||||||
}
|
|
||||||
let t5;
|
|
||||||
if ($[8] !== t3 || $[9] !== t4) {
|
|
||||||
t5 = <>{t3}{t4}</>;
|
|
||||||
$[8] = t3;
|
|
||||||
$[9] = t4;
|
|
||||||
$[10] = t5;
|
|
||||||
} else {
|
|
||||||
t5 = $[10];
|
|
||||||
}
|
|
||||||
return t5;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,37 @@
|
|||||||
import React, { type ReactNode } from 'react';
|
import React, { type ReactNode } from 'react'
|
||||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
|
||||||
import { Box, Text } from '../../ink.js';
|
import { Box, Text } from '../../ink.js'
|
||||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||||
import { Byline } from '../design-system/Byline.js';
|
import { Byline } from '../design-system/Byline.js'
|
||||||
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
|
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
instructions?: ReactNode;
|
instructions?: ReactNode
|
||||||
};
|
}
|
||||||
|
|
||||||
export function WizardNavigationFooter({
|
export function WizardNavigationFooter({
|
||||||
instructions = <Byline>
|
instructions = (
|
||||||
|
<Byline>
|
||||||
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
|
||||||
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
<KeyboardShortcutHint shortcut="Enter" action="select" />
|
||||||
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />
|
<ConfigurableShortcutHint
|
||||||
|
action="confirm:no"
|
||||||
|
context="Confirmation"
|
||||||
|
fallback="Esc"
|
||||||
|
description="go back"
|
||||||
|
/>
|
||||||
</Byline>
|
</Byline>
|
||||||
|
),
|
||||||
}: Props): ReactNode {
|
}: Props): ReactNode {
|
||||||
const exitState = useExitOnCtrlCDWithKeybindings();
|
const exitState = useExitOnCtrlCDWithKeybindings()
|
||||||
return <Box marginLeft={3} marginTop={1}>
|
|
||||||
|
return (
|
||||||
|
<Box marginLeft={3} marginTop={1}>
|
||||||
<Text dimColor>
|
<Text dimColor>
|
||||||
{exitState.pending ? `Press ${exitState.keyName} again to exit` : instructions}
|
{exitState.pending
|
||||||
|
? `Press ${exitState.keyName} again to exit`
|
||||||
|
: instructions}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>;
|
</Box>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,156 +1,96 @@
|
|||||||
import { c as _c } from "react/compiler-runtime";
|
import React, {
|
||||||
import React, { createContext, type ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
createContext,
|
||||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
|
type ReactNode,
|
||||||
import type { WizardContextValue, WizardProviderProps } from './types.js';
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
|
||||||
|
import type { WizardContextValue, WizardProviderProps } from './types.js'
|
||||||
|
|
||||||
// Use any here for the context since it will be cast properly when used
|
// Use any here for the context since it will be cast properly when used
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const WizardContext = createContext<WizardContextValue<any> | null>(null);
|
export const WizardContext = createContext<WizardContextValue<any> | null>(null)
|
||||||
export function WizardProvider(t0) {
|
|
||||||
const $ = _c(38);
|
export function WizardProvider<T extends Record<string, unknown>>({
|
||||||
const {
|
steps,
|
||||||
steps,
|
initialData = {} as T,
|
||||||
initialData: t1,
|
onComplete,
|
||||||
onComplete,
|
onCancel,
|
||||||
onCancel,
|
children,
|
||||||
children,
|
title,
|
||||||
title,
|
showStepCounter = true,
|
||||||
showStepCounter: t2
|
}: WizardProviderProps<T>): ReactNode {
|
||||||
} = t0;
|
const [currentStepIndex, setCurrentStepIndex] = useState(0)
|
||||||
let t3;
|
const [wizardData, setWizardData] = useState<T>(initialData)
|
||||||
if ($[0] !== t1) {
|
const [isCompleted, setIsCompleted] = useState(false)
|
||||||
t3 = t1 === undefined ? {} as T : t1;
|
const [navigationHistory, setNavigationHistory] = useState<number[]>([])
|
||||||
$[0] = t1;
|
|
||||||
$[1] = t3;
|
useExitOnCtrlCDWithKeybindings()
|
||||||
} else {
|
|
||||||
t3 = $[1];
|
// Handle completion in useEffect to avoid updating parent during render
|
||||||
}
|
useEffect(() => {
|
||||||
const initialData = t3;
|
if (isCompleted) {
|
||||||
const showStepCounter = t2 === undefined ? true : t2;
|
setNavigationHistory([])
|
||||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
void onComplete(wizardData)
|
||||||
const [wizardData, setWizardData] = useState(initialData);
|
}
|
||||||
const [isCompleted, setIsCompleted] = useState(false);
|
}, [isCompleted, wizardData, onComplete])
|
||||||
let t4;
|
|
||||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
const goNext = useCallback(() => {
|
||||||
t4 = [];
|
if (currentStepIndex < steps.length - 1) {
|
||||||
$[2] = t4;
|
// If we have history (non-linear flow), add current step to it
|
||||||
} else {
|
|
||||||
t4 = $[2];
|
|
||||||
}
|
|
||||||
const [navigationHistory, setNavigationHistory] = useState(t4);
|
|
||||||
useExitOnCtrlCDWithKeybindings();
|
|
||||||
let t5;
|
|
||||||
let t6;
|
|
||||||
if ($[3] !== isCompleted || $[4] !== onComplete || $[5] !== wizardData) {
|
|
||||||
t5 = () => {
|
|
||||||
if (isCompleted) {
|
|
||||||
setNavigationHistory([]);
|
|
||||||
onComplete(wizardData);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
t6 = [isCompleted, wizardData, onComplete];
|
|
||||||
$[3] = isCompleted;
|
|
||||||
$[4] = onComplete;
|
|
||||||
$[5] = wizardData;
|
|
||||||
$[6] = t5;
|
|
||||||
$[7] = t6;
|
|
||||||
} else {
|
|
||||||
t5 = $[6];
|
|
||||||
t6 = $[7];
|
|
||||||
}
|
|
||||||
useEffect(t5, t6);
|
|
||||||
let t7;
|
|
||||||
if ($[8] !== currentStepIndex || $[9] !== navigationHistory || $[10] !== steps.length) {
|
|
||||||
t7 = () => {
|
|
||||||
if (currentStepIndex < steps.length - 1) {
|
|
||||||
if (navigationHistory.length > 0) {
|
|
||||||
setNavigationHistory(prev => [...prev, currentStepIndex]);
|
|
||||||
}
|
|
||||||
setCurrentStepIndex(_temp);
|
|
||||||
} else {
|
|
||||||
setIsCompleted(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$[8] = currentStepIndex;
|
|
||||||
$[9] = navigationHistory;
|
|
||||||
$[10] = steps.length;
|
|
||||||
$[11] = t7;
|
|
||||||
} else {
|
|
||||||
t7 = $[11];
|
|
||||||
}
|
|
||||||
const goNext = t7;
|
|
||||||
let t8;
|
|
||||||
if ($[12] !== currentStepIndex || $[13] !== navigationHistory || $[14] !== onCancel) {
|
|
||||||
t8 = () => {
|
|
||||||
if (navigationHistory.length > 0) {
|
if (navigationHistory.length > 0) {
|
||||||
const previousStep = navigationHistory[navigationHistory.length - 1];
|
setNavigationHistory(prev => [...prev, currentStepIndex])
|
||||||
if (previousStep !== undefined) {
|
|
||||||
setNavigationHistory(_temp2);
|
|
||||||
setCurrentStepIndex(previousStep);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (currentStepIndex > 0) {
|
|
||||||
setCurrentStepIndex(_temp3);
|
|
||||||
} else {
|
|
||||||
if (onCancel) {
|
|
||||||
onCancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
$[12] = currentStepIndex;
|
setCurrentStepIndex(prev => prev + 1)
|
||||||
$[13] = navigationHistory;
|
} else {
|
||||||
$[14] = onCancel;
|
// Mark as completed, which will trigger useEffect
|
||||||
$[15] = t8;
|
setIsCompleted(true)
|
||||||
} else {
|
}
|
||||||
t8 = $[15];
|
}, [currentStepIndex, steps.length, navigationHistory])
|
||||||
}
|
|
||||||
const goBack = t8;
|
const goBack = useCallback(() => {
|
||||||
let t9;
|
// Check if we have navigation history to use
|
||||||
if ($[16] !== currentStepIndex || $[17] !== steps.length) {
|
if (navigationHistory.length > 0) {
|
||||||
t9 = index => {
|
const previousStep = navigationHistory[navigationHistory.length - 1]
|
||||||
|
if (previousStep !== undefined) {
|
||||||
|
setNavigationHistory(prev => prev.slice(0, -1))
|
||||||
|
setCurrentStepIndex(previousStep)
|
||||||
|
}
|
||||||
|
} else if (currentStepIndex > 0) {
|
||||||
|
// Fallback to simple decrement if no history
|
||||||
|
setCurrentStepIndex(prev => prev - 1)
|
||||||
|
} else if (onCancel) {
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
}, [currentStepIndex, navigationHistory, onCancel])
|
||||||
|
|
||||||
|
const goToStep = useCallback(
|
||||||
|
(index: number) => {
|
||||||
if (index >= 0 && index < steps.length) {
|
if (index >= 0 && index < steps.length) {
|
||||||
setNavigationHistory(prev_3 => [...prev_3, currentStepIndex]);
|
// Push current step to history before jumping
|
||||||
setCurrentStepIndex(index);
|
setNavigationHistory(prev => [...prev, currentStepIndex])
|
||||||
|
setCurrentStepIndex(index)
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
$[16] = currentStepIndex;
|
[currentStepIndex, steps.length],
|
||||||
$[17] = steps.length;
|
)
|
||||||
$[18] = t9;
|
|
||||||
} else {
|
const cancel = useCallback(() => {
|
||||||
t9 = $[18];
|
setNavigationHistory([])
|
||||||
}
|
if (onCancel) {
|
||||||
const goToStep = t9;
|
onCancel()
|
||||||
let t10;
|
}
|
||||||
if ($[19] !== onCancel) {
|
}, [onCancel])
|
||||||
t10 = () => {
|
|
||||||
setNavigationHistory([]);
|
const updateWizardData = useCallback((updates: Partial<T>) => {
|
||||||
if (onCancel) {
|
setWizardData(prev => ({ ...prev, ...updates }))
|
||||||
onCancel();
|
}, [])
|
||||||
}
|
|
||||||
};
|
const contextValue = useMemo<WizardContextValue<T>>(
|
||||||
$[19] = onCancel;
|
() => ({
|
||||||
$[20] = t10;
|
|
||||||
} else {
|
|
||||||
t10 = $[20];
|
|
||||||
}
|
|
||||||
const cancel = t10;
|
|
||||||
let t11;
|
|
||||||
if ($[21] === Symbol.for("react.memo_cache_sentinel")) {
|
|
||||||
t11 = updates => {
|
|
||||||
setWizardData(prev_4 => ({
|
|
||||||
...prev_4,
|
|
||||||
...updates
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
$[21] = t11;
|
|
||||||
} else {
|
|
||||||
t11 = $[21];
|
|
||||||
}
|
|
||||||
const updateWizardData = t11;
|
|
||||||
let t12;
|
|
||||||
if ($[22] !== cancel || $[23] !== currentStepIndex || $[24] !== goBack || $[25] !== goNext || $[26] !== goToStep || $[27] !== showStepCounter || $[28] !== steps.length || $[29] !== title || $[30] !== wizardData) {
|
|
||||||
t12 = {
|
|
||||||
currentStepIndex,
|
currentStepIndex,
|
||||||
totalSteps: steps.length,
|
totalSteps: steps.length,
|
||||||
wizardData,
|
wizardData,
|
||||||
@@ -161,52 +101,31 @@ export function WizardProvider(t0) {
|
|||||||
goToStep,
|
goToStep,
|
||||||
cancel,
|
cancel,
|
||||||
title,
|
title,
|
||||||
showStepCounter
|
showStepCounter,
|
||||||
};
|
}),
|
||||||
$[22] = cancel;
|
[
|
||||||
$[23] = currentStepIndex;
|
currentStepIndex,
|
||||||
$[24] = goBack;
|
steps.length,
|
||||||
$[25] = goNext;
|
wizardData,
|
||||||
$[26] = goToStep;
|
updateWizardData,
|
||||||
$[27] = showStepCounter;
|
goNext,
|
||||||
$[28] = steps.length;
|
goBack,
|
||||||
$[29] = title;
|
goToStep,
|
||||||
$[30] = wizardData;
|
cancel,
|
||||||
$[31] = t12;
|
title,
|
||||||
} else {
|
showStepCounter,
|
||||||
t12 = $[31];
|
],
|
||||||
}
|
)
|
||||||
const contextValue = t12;
|
|
||||||
const CurrentStepComponent = steps[currentStepIndex];
|
const CurrentStepComponent = steps[currentStepIndex]
|
||||||
|
|
||||||
if (!CurrentStepComponent || isCompleted) {
|
if (!CurrentStepComponent || isCompleted) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
let t13;
|
|
||||||
if ($[32] !== CurrentStepComponent || $[33] !== children) {
|
return (
|
||||||
t13 = children || <CurrentStepComponent />;
|
<WizardContext.Provider value={contextValue}>
|
||||||
$[32] = CurrentStepComponent;
|
{children || <CurrentStepComponent />}
|
||||||
$[33] = children;
|
</WizardContext.Provider>
|
||||||
$[34] = t13;
|
)
|
||||||
} else {
|
|
||||||
t13 = $[34];
|
|
||||||
}
|
|
||||||
let t14;
|
|
||||||
if ($[35] !== contextValue || $[36] !== t13) {
|
|
||||||
t14 = <WizardContext.Provider value={contextValue}>{t13}</WizardContext.Provider>;
|
|
||||||
$[35] = contextValue;
|
|
||||||
$[36] = t13;
|
|
||||||
$[37] = t14;
|
|
||||||
} else {
|
|
||||||
t14 = $[37];
|
|
||||||
}
|
|
||||||
return t14;
|
|
||||||
}
|
|
||||||
function _temp3(prev_2) {
|
|
||||||
return prev_2 - 1;
|
|
||||||
}
|
|
||||||
function _temp2(prev_1) {
|
|
||||||
return prev_1.slice(0, -1);
|
|
||||||
}
|
|
||||||
function _temp(prev_0) {
|
|
||||||
return prev_0 + 1;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user