diff --git a/src/components/FeedbackSurvey/FeedbackSurvey.tsx b/src/components/FeedbackSurvey/FeedbackSurvey.tsx
index 8a5ccbfcf..2f9c8e47d 100644
--- a/src/components/FeedbackSurvey/FeedbackSurvey.tsx
+++ b/src/components/FeedbackSurvey/FeedbackSurvey.tsx
@@ -1,173 +1,167 @@
-import { c as _c } from "react/compiler-runtime";
-import React from 'react';
-import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
-import { Box, Text } from '../../ink.js';
-import { FeedbackSurveyView, isValidResponseInput } from './FeedbackSurveyView.js';
-import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
-import { TranscriptSharePrompt } from './TranscriptSharePrompt.js';
-import { useDebouncedDigitInput } from './useDebouncedDigitInput.js';
-import type { FeedbackSurveyResponse } from './utils.js';
+import React from 'react'
+import {
+ type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ logEvent,
+} from 'src/services/analytics/index.js'
+import { Box, Text } from '../../ink.js'
+import {
+ FeedbackSurveyView,
+ isValidResponseInput,
+} from './FeedbackSurveyView.js'
+import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
+import { TranscriptSharePrompt } from './TranscriptSharePrompt.js'
+import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'
+import type { FeedbackSurveyResponse } from './utils.js'
+
type Props = {
- state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
- lastResponse: FeedbackSurveyResponse | null;
- handleSelect: (selected: FeedbackSurveyResponse) => void;
- handleTranscriptSelect?: (selected: TranscriptShareResponse) => void;
- inputValue: string;
- setInputValue: (value: string) => void;
- onRequestFeedback?: () => void;
- message?: string;
-};
-export function FeedbackSurvey(t0) {
- const $ = _c(16);
- const {
- state,
- lastResponse,
- handleSelect,
- handleTranscriptSelect,
- inputValue,
- setInputValue,
- onRequestFeedback,
- message
- } = t0;
- if (state === "closed") {
- return null;
+ state:
+ | 'closed'
+ | 'open'
+ | 'thanks'
+ | 'transcript_prompt'
+ | 'submitting'
+ | 'submitted'
+ lastResponse: FeedbackSurveyResponse | null
+ handleSelect: (selected: FeedbackSurveyResponse) => void
+ handleTranscriptSelect?: (selected: TranscriptShareResponse) => void
+ inputValue: string
+ setInputValue: (value: string) => void
+ onRequestFeedback?: () => void
+ message?: string
+}
+
+export function FeedbackSurvey({
+ state,
+ lastResponse,
+ handleSelect,
+ handleTranscriptSelect,
+ inputValue,
+ setInputValue,
+ onRequestFeedback,
+ message,
+}: Props): React.ReactNode {
+ if (state === 'closed') {
+ return null
}
- if (state === "thanks") {
- let t1;
- if ($[0] !== inputValue || $[1] !== lastResponse || $[2] !== onRequestFeedback || $[3] !== setInputValue) {
- t1 = ;
- $[0] = inputValue;
- $[1] = lastResponse;
- $[2] = onRequestFeedback;
- $[3] = setInputValue;
- $[4] = t1;
- } else {
- t1 = $[4];
- }
- return t1;
+
+ if (state === 'thanks') {
+ return (
+
+ )
}
- if (state === "submitted") {
- let t1;
- if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = {"\u2713"} Thanks for sharing your transcript!;
- $[5] = t1;
- } else {
- t1 = $[5];
- }
- return t1;
+
+ if (state === 'submitted') {
+ return (
+
+
+ {'\u2713'} Thanks for sharing your transcript!
+
+
+ )
}
- if (state === "submitting") {
- let t1;
- if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = Sharing transcript{"\u2026"};
- $[6] = t1;
- } else {
- t1 = $[6];
- }
- return t1;
+
+ if (state === 'submitting') {
+ return (
+
+ Sharing transcript{'\u2026'}
+
+ )
}
- if (state === "transcript_prompt") {
+
+ if (state === 'transcript_prompt') {
if (!handleTranscriptSelect) {
- return null;
+ return null
}
- if (inputValue && !["1", "2", "3"].includes(inputValue)) {
- return null;
+ // Hide prompt if user is typing non-response characters
+ if (inputValue && !['1', '2', '3'].includes(inputValue)) {
+ return null
}
- let t1;
- if ($[7] !== handleTranscriptSelect || $[8] !== inputValue || $[9] !== setInputValue) {
- t1 = ;
- $[7] = handleTranscriptSelect;
- $[8] = inputValue;
- $[9] = setInputValue;
- $[10] = t1;
- } else {
- t1 = $[10];
- }
- return t1;
+ return (
+
+ )
}
+
+ // state === 'open'
+ // Hide the survey if the user is typing anything other than a survey response.
+ // This prevents the survey from showing up when the user is typing a message,
+ // which can result in accidental survey submissions (e.g. "s3cmd").
if (inputValue && !isValidResponseInput(inputValue)) {
- return null;
+ return null
}
- let t1;
- if ($[11] !== handleSelect || $[12] !== inputValue || $[13] !== message || $[14] !== setInputValue) {
- t1 = ;
- $[11] = handleSelect;
- $[12] = inputValue;
- $[13] = message;
- $[14] = setInputValue;
- $[15] = t1;
- } else {
- t1 = $[15];
- }
- return t1;
+
+ return (
+
+ )
}
+
type ThanksProps = {
- lastResponse: FeedbackSurveyResponse | null;
- inputValue: string;
- setInputValue: (value: string) => void;
- onRequestFeedback?: () => void;
-};
-const isFollowUpDigit = (char: string): char is '1' => char === '1';
-function FeedbackSurveyThanks(t0) {
- const $ = _c(12);
- const {
- lastResponse,
+ lastResponse: FeedbackSurveyResponse | null
+ inputValue: string
+ setInputValue: (value: string) => void
+ onRequestFeedback?: () => void
+}
+
+const isFollowUpDigit = (char: string): char is '1' => char === '1'
+
+function FeedbackSurveyThanks({
+ lastResponse,
+ inputValue,
+ setInputValue,
+ onRequestFeedback,
+}: ThanksProps): React.ReactNode {
+ const showFollowUp = onRequestFeedback && lastResponse === 'good'
+
+ // Listen for "1" keypress to launch /feedback
+ useDebouncedDigitInput({
inputValue,
setInputValue,
- onRequestFeedback
- } = t0;
- const showFollowUp = onRequestFeedback && lastResponse === "good";
- const t1 = Boolean(showFollowUp);
- let t2;
- if ($[0] !== lastResponse || $[1] !== onRequestFeedback) {
- t2 = () => {
- logEvent("tengu_feedback_survey_event", {
- event_type: "followup_accepted" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- response: lastResponse as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
- onRequestFeedback?.();
- };
- $[0] = lastResponse;
- $[1] = onRequestFeedback;
- $[2] = t2;
- } else {
- t2 = $[2];
- }
- let t3;
- if ($[3] !== inputValue || $[4] !== setInputValue || $[5] !== t1 || $[6] !== t2) {
- t3 = {
- inputValue,
- setInputValue,
- isValidDigit: isFollowUpDigit,
- enabled: t1,
- once: true,
- onDigit: t2
- };
- $[3] = inputValue;
- $[4] = setInputValue;
- $[5] = t1;
- $[6] = t2;
- $[7] = t3;
- } else {
- t3 = $[7];
- }
- useDebouncedDigitInput(t3);
- const feedbackCommand = false ? "/issue" : "/feedback";
- let t4;
- if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
- t4 = Thanks for the feedback!;
- $[8] = t4;
- } else {
- t4 = $[8];
- }
- let t5;
- if ($[9] !== lastResponse || $[10] !== showFollowUp) {
- t5 = {t4}{showFollowUp ? (Optional) Press [1] to tell us what went well {" \xB7 "}{feedbackCommand} : lastResponse === "bad" ? Use /issue to report model behavior issues. : Use {feedbackCommand} to share detailed feedback anytime.};
- $[9] = lastResponse;
- $[10] = showFollowUp;
- $[11] = t5;
- } else {
- t5 = $[11];
- }
- return t5;
+ isValidDigit: isFollowUpDigit,
+ enabled: Boolean(showFollowUp),
+ once: true,
+ onDigit: () => {
+ logEvent('tengu_feedback_survey_event', {
+ event_type:
+ 'followup_accepted' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ response:
+ lastResponse as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
+ onRequestFeedback?.()
+ },
+ })
+
+ const feedbackCommand =
+ process.env.USER_TYPE === 'ant' ? '/issue' : '/feedback'
+
+ return (
+
+ Thanks for the feedback!
+ {showFollowUp ? (
+
+ (Optional) Press [1] to tell us what
+ went well {' \u00b7 '}
+ {feedbackCommand}
+
+ ) : lastResponse === 'bad' ? (
+ Use /issue to report model behavior issues.
+ ) : (
+
+ Use {feedbackCommand} to share detailed feedback anytime.
+
+ )}
+
+ )
}
diff --git a/src/components/FeedbackSurvey/FeedbackSurveyView.tsx b/src/components/FeedbackSurvey/FeedbackSurveyView.tsx
index 74a6f6bfa..a8eadf3ba 100644
--- a/src/components/FeedbackSurvey/FeedbackSurveyView.tsx
+++ b/src/components/FeedbackSurvey/FeedbackSurveyView.tsx
@@ -1,107 +1,72 @@
-import { c as _c } from "react/compiler-runtime";
-import React from 'react';
-import { Box, Text } from '../../ink.js';
-import { useDebouncedDigitInput } from './useDebouncedDigitInput.js';
-import type { FeedbackSurveyResponse } from './utils.js';
+import React from 'react'
+import { Box, Text } from '../../ink.js'
+import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'
+import type { FeedbackSurveyResponse } from './utils.js'
+
type Props = {
- onSelect: (option: FeedbackSurveyResponse) => void;
- inputValue: string;
- setInputValue: (value: string) => void;
- message?: string;
-};
-const RESPONSE_INPUTS = ['0', '1', '2', '3'] as const;
-type ResponseInput = (typeof RESPONSE_INPUTS)[number];
+ onSelect: (option: FeedbackSurveyResponse) => void
+ inputValue: string
+ setInputValue: (value: string) => void
+ message?: string
+}
+
+const RESPONSE_INPUTS = ['0', '1', '2', '3'] as const
+type ResponseInput = (typeof RESPONSE_INPUTS)[number]
+
const inputToResponse: Record = {
'0': 'dismissed',
'1': 'bad',
'2': 'fine',
- '3': 'good'
-} as const;
-export const isValidResponseInput = (input: string): input is ResponseInput => (RESPONSE_INPUTS as readonly string[]).includes(input);
-const DEFAULT_MESSAGE = 'How is Claude doing this session? (optional)';
-export function FeedbackSurveyView(t0) {
- const $ = _c(15);
- const {
- onSelect,
+ '3': 'good',
+} as const
+
+export const isValidResponseInput = (input: string): input is ResponseInput =>
+ (RESPONSE_INPUTS as readonly string[]).includes(input)
+
+const DEFAULT_MESSAGE = 'How is Claude doing this session? (optional)'
+
+export function FeedbackSurveyView({
+ onSelect,
+ inputValue,
+ setInputValue,
+ message = DEFAULT_MESSAGE,
+}: Props): React.ReactNode {
+ useDebouncedDigitInput({
inputValue,
setInputValue,
- message: t1
- } = t0;
- const message = t1 === undefined ? DEFAULT_MESSAGE : t1;
- let t2;
- if ($[0] !== onSelect) {
- t2 = digit => onSelect(inputToResponse[digit]);
- $[0] = onSelect;
- $[1] = t2;
- } else {
- t2 = $[1];
- }
- let t3;
- if ($[2] !== inputValue || $[3] !== setInputValue || $[4] !== t2) {
- t3 = {
- inputValue,
- setInputValue,
- isValidDigit: isValidResponseInput,
- onDigit: t2
- };
- $[2] = inputValue;
- $[3] = setInputValue;
- $[4] = t2;
- $[5] = t3;
- } else {
- t3 = $[5];
- }
- useDebouncedDigitInput(t3);
- let t4;
- if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
- t4 = ● ;
- $[6] = t4;
- } else {
- t4 = $[6];
- }
- let t5;
- if ($[7] !== message) {
- t5 = {t4}{message};
- $[7] = message;
- $[8] = t5;
- } else {
- t5 = $[8];
- }
- let t6;
- if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
- t6 = 1: Bad;
- $[9] = t6;
- } else {
- t6 = $[9];
- }
- let t7;
- if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
- t7 = 2: Fine;
- $[10] = t7;
- } else {
- t7 = $[10];
- }
- let t8;
- if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
- t8 = 3: Good;
- $[11] = t8;
- } else {
- t8 = $[11];
- }
- let t9;
- if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
- t9 = {t6}{t7}{t8}0: Dismiss;
- $[12] = t9;
- } else {
- t9 = $[12];
- }
- let t10;
- if ($[13] !== t5) {
- t10 = {t5}{t9};
- $[13] = t5;
- $[14] = t10;
- } else {
- t10 = $[14];
- }
- return t10;
+ isValidDigit: isValidResponseInput,
+ onDigit: digit => onSelect(inputToResponse[digit]),
+ })
+
+ return (
+
+
+ ●
+ {message}
+
+
+
+
+
+ 1: Bad
+
+
+
+
+ 2: Fine
+
+
+
+
+ 3: Good
+
+
+
+
+ 0: Dismiss
+
+
+
+
+ )
}
diff --git a/src/components/FeedbackSurvey/TranscriptSharePrompt.tsx b/src/components/FeedbackSurvey/TranscriptSharePrompt.tsx
index da3893a76..ec7a974f5 100644
--- a/src/components/FeedbackSurvey/TranscriptSharePrompt.tsx
+++ b/src/components/FeedbackSurvey/TranscriptSharePrompt.tsx
@@ -1,87 +1,74 @@
-import { c as _c } from "react/compiler-runtime";
-import React from 'react';
-import { BLACK_CIRCLE } from '../../constants/figures.js';
-import { Box, Text } from '../../ink.js';
-import { useDebouncedDigitInput } from './useDebouncedDigitInput.js';
-export type TranscriptShareResponse = 'yes' | 'no' | 'dont_ask_again';
+import React from 'react'
+import { BLACK_CIRCLE } from '../../constants/figures.js'
+import { Box, Text } from '../../ink.js'
+import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'
+
+export type TranscriptShareResponse = 'yes' | 'no' | 'dont_ask_again'
+
type Props = {
- onSelect: (option: TranscriptShareResponse) => void;
- inputValue: string;
- setInputValue: (value: string) => void;
-};
-const RESPONSE_INPUTS = ['1', '2', '3'] as const;
-type ResponseInput = (typeof RESPONSE_INPUTS)[number];
+ onSelect: (option: TranscriptShareResponse) => void
+ inputValue: string
+ setInputValue: (value: string) => void
+}
+
+const RESPONSE_INPUTS = ['1', '2', '3'] as const
+type ResponseInput = (typeof RESPONSE_INPUTS)[number]
+
const inputToResponse: Record = {
'1': 'yes',
'2': 'no',
- '3': 'dont_ask_again'
-} as const;
-const isValidResponseInput = (input: string): input is ResponseInput => (RESPONSE_INPUTS as readonly string[]).includes(input);
-export function TranscriptSharePrompt(t0) {
- const $ = _c(11);
- const {
- onSelect,
+ '3': 'dont_ask_again',
+} as const
+
+const isValidResponseInput = (input: string): input is ResponseInput =>
+ (RESPONSE_INPUTS as readonly string[]).includes(input)
+
+export function TranscriptSharePrompt({
+ onSelect,
+ inputValue,
+ setInputValue,
+}: Props): React.ReactNode {
+ useDebouncedDigitInput({
inputValue,
- setInputValue
- } = t0;
- let t1;
- if ($[0] !== onSelect) {
- t1 = digit => onSelect(inputToResponse[digit]);
- $[0] = onSelect;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- let t2;
- if ($[2] !== inputValue || $[3] !== setInputValue || $[4] !== t1) {
- t2 = {
- inputValue,
- setInputValue,
- isValidDigit: isValidResponseInput,
- onDigit: t1
- };
- $[2] = inputValue;
- $[3] = setInputValue;
- $[4] = t1;
- $[5] = t2;
- } else {
- t2 = $[5];
- }
- useDebouncedDigitInput(t2);
- let t3;
- if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
- t3 = {BLACK_CIRCLE} Can Anthropic look at your session transcript to help us improve Claude Code?;
- $[6] = t3;
- } else {
- t3 = $[6];
- }
- let t4;
- if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
- t4 = Learn more: https://code.claude.com/docs/en/data-usage#session-quality-surveys;
- $[7] = t4;
- } else {
- t4 = $[7];
- }
- let t5;
- if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
- t5 = 1: Yes;
- $[8] = t5;
- } else {
- t5 = $[8];
- }
- let t6;
- if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
- t6 = 2: No;
- $[9] = t6;
- } else {
- t6 = $[9];
- }
- let t7;
- if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
- t7 = {t3}{t4}{t5}{t6}3: Don't ask again;
- $[10] = t7;
- } else {
- t7 = $[10];
- }
- return t7;
+ setInputValue,
+ isValidDigit: isValidResponseInput,
+ onDigit: digit => onSelect(inputToResponse[digit]),
+ })
+
+ return (
+
+
+ {BLACK_CIRCLE}
+
+ Can Anthropic look at your session transcript to help us improve
+ Claude Code?
+
+
+
+
+
+ Learn more:
+ https://code.claude.com/docs/en/data-usage#session-quality-surveys
+
+
+
+
+
+
+ 1: Yes
+
+
+
+
+ 2: No
+
+
+
+
+ 3: Don't ask again
+
+
+
+
+ )
}
diff --git a/src/components/FeedbackSurvey/useFeedbackSurvey.tsx b/src/components/FeedbackSurvey/useFeedbackSurvey.tsx
index d19cd0300..166c2dcdd 100644
--- a/src/components/FeedbackSurvey/useFeedbackSurvey.tsx
+++ b/src/components/FeedbackSurvey/useFeedbackSurvey.tsx
@@ -1,32 +1,41 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { useDynamicConfig } from 'src/hooks/useDynamicConfig.js';
-import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js';
-import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
-import { isPolicyAllowed } from '../../services/policyLimits/index.js';
-import type { Message } from '../../types/message.js';
-import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
-import { isEnvTruthy } from '../../utils/envUtils.js';
-import { getLastAssistantMessage } from '../../utils/messages.js';
-import { getMainLoopModel } from '../../utils/model/model.js';
-import { getInitialSettings } from '../../utils/settings/settings.js';
-import { logOTelEvent } from '../../utils/telemetry/events.js';
-import { submitTranscriptShare, type TranscriptShareTrigger } from './submitTranscriptShare.js';
-import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
-import { useSurveyState } from './useSurveyState.js';
-import type { FeedbackSurveyResponse, FeedbackSurveyType } from './utils.js';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { useDynamicConfig } from 'src/hooks/useDynamicConfig.js'
+import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'
+import {
+ type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ logEvent,
+} from 'src/services/analytics/index.js'
+import { isPolicyAllowed } from '../../services/policyLimits/index.js'
+import type { Message } from '../../types/message.js'
+import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
+import { isEnvTruthy } from '../../utils/envUtils.js'
+import { getLastAssistantMessage } from '../../utils/messages.js'
+import { getMainLoopModel } from '../../utils/model/model.js'
+import { getInitialSettings } from '../../utils/settings/settings.js'
+import { logOTelEvent } from '../../utils/telemetry/events.js'
+import {
+ submitTranscriptShare,
+ type TranscriptShareTrigger,
+} from './submitTranscriptShare.js'
+import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
+import { useSurveyState } from './useSurveyState.js'
+import type { FeedbackSurveyResponse, FeedbackSurveyType } from './utils.js'
+
type FeedbackSurveyConfig = {
- minTimeBeforeFeedbackMs: number;
- minTimeBetweenFeedbackMs: number;
- minTimeBetweenGlobalFeedbackMs: number;
- minUserTurnsBeforeFeedback: number;
- minUserTurnsBetweenFeedback: number;
- hideThanksAfterMs: number;
- onForModels: string[];
- probability: number;
-};
+ minTimeBeforeFeedbackMs: number
+ minTimeBetweenFeedbackMs: number
+ minTimeBetweenGlobalFeedbackMs: number
+ minUserTurnsBeforeFeedback: number
+ minUserTurnsBetweenFeedback: number
+ hideThanksAfterMs: number
+ onForModels: string[]
+ probability: number
+}
+
type TranscriptAskConfig = {
- probability: number;
-};
+ probability: number
+}
+
const DEFAULT_FEEDBACK_SURVEY_CONFIG: FeedbackSurveyConfig = {
minTimeBeforeFeedbackMs: 600000,
minTimeBetweenFeedbackMs: 3600000,
@@ -35,261 +44,381 @@ const DEFAULT_FEEDBACK_SURVEY_CONFIG: FeedbackSurveyConfig = {
minUserTurnsBetweenFeedback: 10,
hideThanksAfterMs: 3000,
onForModels: ['*'],
- probability: 0.005
-};
+ probability: 0.005,
+}
+
const DEFAULT_TRANSCRIPT_ASK_CONFIG: TranscriptAskConfig = {
- probability: 0
-};
-export function useFeedbackSurvey(messages: Message[], isLoading: boolean, submitCount: number, surveyType: FeedbackSurveyType = 'session', hasActivePrompt: boolean = false): {
- state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
- lastResponse: FeedbackSurveyResponse | null;
- handleSelect: (selected: FeedbackSurveyResponse) => boolean;
- handleTranscriptSelect: (selected: TranscriptShareResponse) => void;
+ probability: 0,
+}
+
+export function useFeedbackSurvey(
+ messages: Message[],
+ isLoading: boolean,
+ submitCount: number,
+ surveyType: FeedbackSurveyType = 'session',
+ hasActivePrompt: boolean = false,
+): {
+ state:
+ | 'closed'
+ | 'open'
+ | 'thanks'
+ | 'transcript_prompt'
+ | 'submitting'
+ | 'submitted'
+ lastResponse: FeedbackSurveyResponse | null
+ handleSelect: (selected: FeedbackSurveyResponse) => boolean
+ handleTranscriptSelect: (selected: TranscriptShareResponse) => void
} {
- const lastAssistantMessageIdRef = useRef('unknown');
- lastAssistantMessageIdRef.current = getLastAssistantMessage(messages)?.message?.id || 'unknown';
+ const lastAssistantMessageIdRef = useRef('unknown')
+ lastAssistantMessageIdRef.current =
+ getLastAssistantMessage(messages)?.message?.id || 'unknown'
const [feedbackSurvey, setFeedbackSurvey] = useState<{
- timeLastShown: number | null;
- submitCountAtLastAppearance: number | null;
- }>(() => ({
- timeLastShown: null,
- submitCountAtLastAppearance: null
- }));
- const config = useDynamicConfig('tengu_feedback_survey_config', DEFAULT_FEEDBACK_SURVEY_CONFIG);
- const badTranscriptAskConfig = useDynamicConfig('tengu_bad_survey_transcript_ask_config', DEFAULT_TRANSCRIPT_ASK_CONFIG);
- const goodTranscriptAskConfig = useDynamicConfig('tengu_good_survey_transcript_ask_config', DEFAULT_TRANSCRIPT_ASK_CONFIG);
- const settingsRate = getInitialSettings().feedbackSurveyRate;
- const sessionStartTime = useRef(Date.now());
- const submitCountAtSessionStart = useRef(submitCount);
- const submitCountRef = useRef(submitCount);
- submitCountRef.current = submitCount;
- const messagesRef = useRef(messages);
- messagesRef.current = messages;
+ timeLastShown: number | null
+ submitCountAtLastAppearance: number | null
+ }>(() => ({ timeLastShown: null, submitCountAtLastAppearance: null }))
+ const config = useDynamicConfig(
+ 'tengu_feedback_survey_config',
+ DEFAULT_FEEDBACK_SURVEY_CONFIG,
+ )
+ const badTranscriptAskConfig = useDynamicConfig(
+ 'tengu_bad_survey_transcript_ask_config',
+ DEFAULT_TRANSCRIPT_ASK_CONFIG,
+ )
+ const goodTranscriptAskConfig = useDynamicConfig(
+ 'tengu_good_survey_transcript_ask_config',
+ DEFAULT_TRANSCRIPT_ASK_CONFIG,
+ )
+ const settingsRate = getInitialSettings().feedbackSurveyRate
+ const sessionStartTime = useRef(Date.now())
+ const submitCountAtSessionStart = useRef(submitCount)
+ const submitCountRef = useRef(submitCount)
+ submitCountRef.current = submitCount
+ const messagesRef = useRef(messages)
+ messagesRef.current = messages
// Probability gate: roll once when eligibility conditions are met, not on every
// useMemo re-evaluation. Without this, each dependency change (submitCount,
// isLoading toggle, etc.) re-rolls Math.random(), making the survey almost
// certain to appear after enough renders.
- const probabilityPassedRef = useRef(false);
- const lastEligibleSubmitCountRef = useRef(null);
- const updateLastShownTime = useCallback((timestamp: number, submitCountValue: number) => {
- setFeedbackSurvey(prev => {
- if (prev.timeLastShown === timestamp && prev.submitCountAtLastAppearance === submitCountValue) {
- return prev;
- }
- return {
- timeLastShown: timestamp,
- submitCountAtLastAppearance: submitCountValue
- };
- });
- // Persist cross-session pacing state (previously done by onChangeAppState observer)
- if (getGlobalConfig().feedbackSurveyState?.lastShownTime !== timestamp) {
- saveGlobalConfig(current => ({
- ...current,
- feedbackSurveyState: {
- lastShownTime: timestamp
+ const probabilityPassedRef = useRef(false)
+ const lastEligibleSubmitCountRef = useRef(null)
+
+ const updateLastShownTime = useCallback(
+ (timestamp: number, submitCountValue: number) => {
+ setFeedbackSurvey(prev => {
+ if (
+ prev.timeLastShown === timestamp &&
+ prev.submitCountAtLastAppearance === submitCountValue
+ ) {
+ return prev
}
- }));
- }
- }, []);
- const onOpen = useCallback((appearanceId: string) => {
- updateLastShownTime(Date.now(), submitCountRef.current);
- logEvent('tengu_feedback_survey_event', {
- event_type: 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
- void logOTelEvent('feedback_survey', {
- event_type: 'appeared',
- appearance_id: appearanceId,
- survey_type: surveyType
- });
- }, [updateLastShownTime, surveyType]);
- const onSelect = useCallback((appearanceId_0: string, selected: FeedbackSurveyResponse) => {
- updateLastShownTime(Date.now(), submitCountRef.current);
- logEvent('tengu_feedback_survey_event', {
- event_type: 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
- void logOTelEvent('feedback_survey', {
- event_type: 'responded',
- appearance_id: appearanceId_0,
- response: selected,
- survey_type: surveyType
- });
- }, [updateLastShownTime, surveyType]);
- const shouldShowTranscriptPrompt = useCallback((selected_0: FeedbackSurveyResponse) => {
- // Only bad and good ratings trigger the transcript ask
- if (selected_0 !== 'bad' && selected_0 !== 'good') {
- return false;
- }
+ return {
+ timeLastShown: timestamp,
+ submitCountAtLastAppearance: submitCountValue,
+ }
+ })
+ // Persist cross-session pacing state (previously done by onChangeAppState observer)
+ if (getGlobalConfig().feedbackSurveyState?.lastShownTime !== timestamp) {
+ saveGlobalConfig(current => ({
+ ...current,
+ feedbackSurveyState: {
+ lastShownTime: timestamp,
+ },
+ }))
+ }
+ },
+ [],
+ )
- // Don't show if user previously chose "Don't ask again"
- if (getGlobalConfig().transcriptShareDismissed) {
- return false;
- }
-
- // Don't show if product feedback is blocked by org policy (ZDR)
- if (!isPolicyAllowed('allow_product_feedback')) {
- return false;
- }
-
- // Probability gate from GrowthBook config (separate per rating)
- const probability = selected_0 === 'bad' ? badTranscriptAskConfig.probability : goodTranscriptAskConfig.probability;
- return Math.random() <= probability;
- }, [badTranscriptAskConfig.probability, goodTranscriptAskConfig.probability]);
- const onTranscriptPromptShown = useCallback((appearanceId_1: string, surveyResponse: FeedbackSurveyResponse) => {
- const trigger: TranscriptShareTrigger = surveyResponse === 'good' ? 'good_feedback_survey' : 'bad_feedback_survey';
- logEvent('tengu_feedback_survey_event', {
- event_type: 'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- appearance_id: appearanceId_1 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- trigger: trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
- void logOTelEvent('feedback_survey', {
- event_type: 'transcript_prompt_appeared',
- appearance_id: appearanceId_1,
- survey_type: surveyType
- });
- }, [surveyType]);
- const onTranscriptSelect = useCallback(async (appearanceId_2: string, selected_1: TranscriptShareResponse, surveyResponse_0: FeedbackSurveyResponse | null): Promise => {
- const trigger_0: TranscriptShareTrigger = surveyResponse_0 === 'good' ? 'good_feedback_survey' : 'bad_feedback_survey';
- logEvent('tengu_feedback_survey_event', {
- event_type: `transcript_share_${selected_1}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- trigger: trigger_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
- if (selected_1 === 'dont_ask_again') {
- saveGlobalConfig(current_0 => ({
- ...current_0,
- transcriptShareDismissed: true
- }));
- }
- if (selected_1 === 'yes') {
- const result = await submitTranscriptShare(messagesRef.current, trigger_0, appearanceId_2);
+ const onOpen = useCallback(
+ (appearanceId: string) => {
+ updateLastShownTime(Date.now(), submitCountRef.current)
logEvent('tengu_feedback_survey_event', {
- event_type: (result.success ? 'transcript_share_submitted' : 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- trigger: trigger_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
- return result.success;
- }
- return false;
- }, [surveyType]);
- const {
- state,
- lastResponse,
- open,
- handleSelect,
- handleTranscriptSelect
- } = useSurveyState({
- hideThanksAfterMs: config.hideThanksAfterMs,
- onOpen,
- onSelect,
- shouldShowTranscriptPrompt,
- onTranscriptPromptShown,
- onTranscriptSelect
- });
- const currentModel = getMainLoopModel();
+ event_type:
+ 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ appearance_id:
+ appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ last_assistant_message_id:
+ lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ survey_type:
+ surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
+ void logOTelEvent('feedback_survey', {
+ event_type: 'appeared',
+ appearance_id: appearanceId,
+ survey_type: surveyType,
+ })
+ },
+ [updateLastShownTime, surveyType],
+ )
+
+ const onSelect = useCallback(
+ (appearanceId: string, selected: FeedbackSurveyResponse) => {
+ updateLastShownTime(Date.now(), submitCountRef.current)
+ logEvent('tengu_feedback_survey_event', {
+ event_type:
+ 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ appearance_id:
+ appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ response:
+ selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ last_assistant_message_id:
+ lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ survey_type:
+ surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
+ void logOTelEvent('feedback_survey', {
+ event_type: 'responded',
+ appearance_id: appearanceId,
+ response: selected,
+ survey_type: surveyType,
+ })
+ },
+ [updateLastShownTime, surveyType],
+ )
+
+ const shouldShowTranscriptPrompt = useCallback(
+ (selected: FeedbackSurveyResponse) => {
+ // Only bad and good ratings trigger the transcript ask
+ if (selected !== 'bad' && selected !== 'good') {
+ return false
+ }
+
+ // Don't show if user previously chose "Don't ask again"
+ if (getGlobalConfig().transcriptShareDismissed) {
+ return false
+ }
+
+ // Don't show if product feedback is blocked by org policy (ZDR)
+ if (!isPolicyAllowed('allow_product_feedback')) {
+ return false
+ }
+
+ // Probability gate from GrowthBook config (separate per rating)
+ const probability =
+ selected === 'bad'
+ ? badTranscriptAskConfig.probability
+ : goodTranscriptAskConfig.probability
+ return Math.random() <= probability
+ },
+ [badTranscriptAskConfig.probability, goodTranscriptAskConfig.probability],
+ )
+
+ const onTranscriptPromptShown = useCallback(
+ (appearanceId: string, surveyResponse: FeedbackSurveyResponse) => {
+ const trigger: TranscriptShareTrigger =
+ surveyResponse === 'good'
+ ? 'good_feedback_survey'
+ : 'bad_feedback_survey'
+ logEvent('tengu_feedback_survey_event', {
+ event_type:
+ 'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ appearance_id:
+ appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ last_assistant_message_id:
+ lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ survey_type:
+ surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ trigger:
+ trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
+ void logOTelEvent('feedback_survey', {
+ event_type: 'transcript_prompt_appeared',
+ appearance_id: appearanceId,
+ survey_type: surveyType,
+ })
+ },
+ [surveyType],
+ )
+
+ const onTranscriptSelect = useCallback(
+ async (
+ appearanceId: string,
+ selected: TranscriptShareResponse,
+ surveyResponse: FeedbackSurveyResponse | null,
+ ): Promise => {
+ const trigger: TranscriptShareTrigger =
+ surveyResponse === 'good'
+ ? 'good_feedback_survey'
+ : 'bad_feedback_survey'
+
+ logEvent('tengu_feedback_survey_event', {
+ event_type:
+ `transcript_share_${selected}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ appearance_id:
+ appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ last_assistant_message_id:
+ lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ survey_type:
+ surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ trigger:
+ trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
+
+ if (selected === 'dont_ask_again') {
+ saveGlobalConfig(current => ({
+ ...current,
+ transcriptShareDismissed: true,
+ }))
+ }
+
+ if (selected === 'yes') {
+ const result = await submitTranscriptShare(
+ messagesRef.current,
+ trigger,
+ appearanceId,
+ )
+ logEvent('tengu_feedback_survey_event', {
+ event_type: (result.success
+ ? 'transcript_share_submitted'
+ : 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ appearance_id:
+ appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ trigger:
+ trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
+ return result.success
+ }
+
+ return false
+ },
+ [surveyType],
+ )
+
+ const { state, lastResponse, open, handleSelect, handleTranscriptSelect } =
+ useSurveyState({
+ hideThanksAfterMs: config.hideThanksAfterMs,
+ onOpen,
+ onSelect,
+ shouldShowTranscriptPrompt,
+ onTranscriptPromptShown,
+ onTranscriptSelect,
+ })
+
+ const currentModel = getMainLoopModel()
const isModelAllowed = useMemo(() => {
if (config.onForModels.length === 0) {
- return false;
+ return false
}
if (config.onForModels.includes('*')) {
- return true;
+ return true
}
- return config.onForModels.includes(currentModel);
- }, [config.onForModels, currentModel]);
+ return config.onForModels.includes(currentModel)
+ }, [config.onForModels, currentModel])
+
const shouldOpen = useMemo(() => {
if (state !== 'closed') {
- return false;
+ return false
}
+
if (isLoading) {
- return false;
+ return false
}
// Don't show survey when permission or ask question prompts are visible
if (hasActivePrompt) {
- return false;
+ return false
}
// Force display for testing
- if (process.env.CLAUDE_FORCE_DISPLAY_SURVEY && !feedbackSurvey.timeLastShown) {
- return true;
+ if (
+ process.env.CLAUDE_FORCE_DISPLAY_SURVEY &&
+ !feedbackSurvey.timeLastShown
+ ) {
+ return true
}
+
if (!isModelAllowed) {
- return false;
+ return false
}
+
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
- return false;
+ return false
}
+
if (isFeedbackSurveyDisabled()) {
- return false;
+ return false
}
// Check if product feedback is allowed by org policy
if (!isPolicyAllowed('allow_product_feedback')) {
- return false;
+ return false
}
// Check session-local pacing
if (feedbackSurvey.timeLastShown) {
// Check time elapsed since last appearance in this session
- const timeSinceLastShown = Date.now() - feedbackSurvey.timeLastShown;
+ const timeSinceLastShown = Date.now() - feedbackSurvey.timeLastShown
if (timeSinceLastShown < config.minTimeBetweenFeedbackMs) {
- return false;
+ return false
}
// Check user turn requirement for subsequent appearances
- if (feedbackSurvey.submitCountAtLastAppearance !== null && submitCount < feedbackSurvey.submitCountAtLastAppearance + config.minUserTurnsBetweenFeedback) {
- return false;
+ if (
+ feedbackSurvey.submitCountAtLastAppearance !== null &&
+ submitCount <
+ feedbackSurvey.submitCountAtLastAppearance +
+ config.minUserTurnsBetweenFeedback
+ ) {
+ return false
}
} else {
// First appearance in this session
- const timeSinceSessionStart = Date.now() - sessionStartTime.current;
+ const timeSinceSessionStart = Date.now() - sessionStartTime.current
if (timeSinceSessionStart < config.minTimeBeforeFeedbackMs) {
- return false;
+ return false
}
- if (submitCount < submitCountAtSessionStart.current + config.minUserTurnsBeforeFeedback) {
- return false;
+ if (
+ submitCount <
+ submitCountAtSessionStart.current + config.minUserTurnsBeforeFeedback
+ ) {
+ return false
}
}
// Probability check: roll once per eligibility window to avoid re-rolling
// on every useMemo re-evaluation (which would make triggering near-certain).
if (lastEligibleSubmitCountRef.current !== submitCount) {
- lastEligibleSubmitCountRef.current = submitCount;
- probabilityPassedRef.current = Math.random() <= (settingsRate ?? config.probability);
+ lastEligibleSubmitCountRef.current = submitCount
+ probabilityPassedRef.current =
+ Math.random() <= (settingsRate ?? config.probability)
}
if (!probabilityPassedRef.current) {
- return false;
+ return false
}
// Check global pacing (across all sessions)
// Leave this till last because it reads from the filesystem which is expensive.
- const globalFeedbackState = getGlobalConfig().feedbackSurveyState;
+ const globalFeedbackState = getGlobalConfig().feedbackSurveyState
if (globalFeedbackState?.lastShownTime) {
- const timeSinceGlobalLastShown = Date.now() - globalFeedbackState.lastShownTime;
+ const timeSinceGlobalLastShown =
+ Date.now() - globalFeedbackState.lastShownTime
if (timeSinceGlobalLastShown < config.minTimeBetweenGlobalFeedbackMs) {
- return false;
+ return false
}
}
- return true;
- }, [state, isLoading, hasActivePrompt, isModelAllowed, feedbackSurvey.timeLastShown, feedbackSurvey.submitCountAtLastAppearance, submitCount, config.minTimeBetweenFeedbackMs, config.minTimeBetweenGlobalFeedbackMs, config.minUserTurnsBetweenFeedback, config.minTimeBeforeFeedbackMs, config.minUserTurnsBeforeFeedback, config.probability, settingsRate]);
+
+ return true
+ }, [
+ state,
+ isLoading,
+ hasActivePrompt,
+ isModelAllowed,
+ feedbackSurvey.timeLastShown,
+ feedbackSurvey.submitCountAtLastAppearance,
+ submitCount,
+ config.minTimeBetweenFeedbackMs,
+ config.minTimeBetweenGlobalFeedbackMs,
+ config.minUserTurnsBetweenFeedback,
+ config.minTimeBeforeFeedbackMs,
+ config.minUserTurnsBeforeFeedback,
+ config.probability,
+ settingsRate,
+ ])
+
useEffect(() => {
if (shouldOpen) {
- open();
+ open()
}
- }, [shouldOpen, open]);
- return {
- state,
- lastResponse,
- handleSelect,
- handleTranscriptSelect
- };
+ }, [shouldOpen, open])
+
+ return { state, lastResponse, handleSelect, handleTranscriptSelect }
}
diff --git a/src/components/FeedbackSurvey/useMemorySurvey.tsx b/src/components/FeedbackSurvey/useMemorySurvey.tsx
index bd28ee699..2c96b2b9e 100644
--- a/src/components/FeedbackSurvey/useMemorySurvey.tsx
+++ b/src/components/FeedbackSurvey/useMemorySurvey.tsx
@@ -1,212 +1,283 @@
-import { useCallback, useEffect, useMemo, useRef } from 'react';
-import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js';
-import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js';
-import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
-import { isAutoMemoryEnabled } from '../../memdir/paths.js';
-import { isPolicyAllowed } from '../../services/policyLimits/index.js';
-import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js';
-import type { Message } from '../../types/message.js';
-import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
-import { isEnvTruthy } from '../../utils/envUtils.js';
-import { isAutoManagedMemoryFile } from '../../utils/memoryFileDetection.js';
-import { extractTextContent, getLastAssistantMessage } from '../../utils/messages.js';
-import { logOTelEvent } from '../../utils/telemetry/events.js';
-import { submitTranscriptShare } from './submitTranscriptShare.js';
-import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
-import { useSurveyState } from './useSurveyState.js';
-import type { FeedbackSurveyResponse } from './utils.js';
-const HIDE_THANKS_AFTER_MS = 3000;
-const MEMORY_SURVEY_GATE = 'tengu_dunwich_bell';
-const MEMORY_SURVEY_EVENT = 'tengu_memory_survey_event';
-const SURVEY_PROBABILITY = 0.2;
-const TRANSCRIPT_SHARE_TRIGGER = 'memory_survey';
-const MEMORY_WORD_RE = /\bmemor(?:y|ies)\b/i;
+import { useCallback, useEffect, useMemo, useRef } from 'react'
+import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'
+import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
+import {
+ type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ logEvent,
+} from 'src/services/analytics/index.js'
+import { isAutoMemoryEnabled } from '../../memdir/paths.js'
+import { isPolicyAllowed } from '../../services/policyLimits/index.js'
+import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js'
+import type { Message } from '../../types/message.js'
+import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
+import { isEnvTruthy } from '../../utils/envUtils.js'
+import { isAutoManagedMemoryFile } from '../../utils/memoryFileDetection.js'
+import {
+ extractTextContent,
+ getLastAssistantMessage,
+} from '../../utils/messages.js'
+import { logOTelEvent } from '../../utils/telemetry/events.js'
+import { submitTranscriptShare } from './submitTranscriptShare.js'
+import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
+import { useSurveyState } from './useSurveyState.js'
+import type { FeedbackSurveyResponse } from './utils.js'
+
+const HIDE_THANKS_AFTER_MS = 3000
+const MEMORY_SURVEY_GATE = 'tengu_dunwich_bell'
+const MEMORY_SURVEY_EVENT = 'tengu_memory_survey_event'
+const SURVEY_PROBABILITY = 0.2
+const TRANSCRIPT_SHARE_TRIGGER = 'memory_survey'
+
+const MEMORY_WORD_RE = /\bmemor(?:y|ies)\b/i
+
function hasMemoryFileRead(messages: Message[]): boolean {
for (const message of messages) {
if (message.type !== 'assistant') {
- continue;
+ continue
}
- const content = message.message.content;
+ const content = message.message.content
if (!Array.isArray(content)) {
- continue;
+ continue
}
for (const block of content) {
if (block.type !== 'tool_use' || block.name !== FILE_READ_TOOL_NAME) {
- continue;
+ continue
}
- const input = block.input as {
- file_path?: unknown;
- };
- if (typeof input.file_path === 'string' && isAutoManagedMemoryFile(input.file_path)) {
- return true;
+ const input = block.input as { file_path?: unknown }
+ if (
+ typeof input.file_path === 'string' &&
+ isAutoManagedMemoryFile(input.file_path)
+ ) {
+ return true
}
}
}
- return false;
+ return false
}
-export function useMemorySurvey(messages: Message[], isLoading: boolean, hasActivePrompt = false, {
- enabled = true
-}: {
- enabled?: boolean;
-} = {}): {
- state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
- lastResponse: FeedbackSurveyResponse | null;
- handleSelect: (selected: FeedbackSurveyResponse) => void;
- handleTranscriptSelect: (selected: TranscriptShareResponse) => void;
+
+export function useMemorySurvey(
+ messages: Message[],
+ isLoading: boolean,
+ hasActivePrompt = false,
+ { enabled = true }: { enabled?: boolean } = {},
+): {
+ state:
+ | 'closed'
+ | 'open'
+ | 'thanks'
+ | 'transcript_prompt'
+ | 'submitting'
+ | 'submitted'
+ lastResponse: FeedbackSurveyResponse | null
+ handleSelect: (selected: FeedbackSurveyResponse) => void
+ handleTranscriptSelect: (selected: TranscriptShareResponse) => void
} {
// Track assistant message UUIDs that were already evaluated so we don't
// re-roll probability on re-renders or re-scan messages for the same turn.
- const seenAssistantUuids = useRef>(new Set());
+ const seenAssistantUuids = useRef>(new Set())
// Once a memory file read is observed it stays true for the session —
// skip the O(n) scan on subsequent turns.
- const memoryReadSeen = useRef(false);
- const messagesRef = useRef(messages);
- messagesRef.current = messages;
+ const memoryReadSeen = useRef(false)
+ const messagesRef = useRef(messages)
+ messagesRef.current = messages
+
const onOpen = useCallback((appearanceId: string) => {
logEvent(MEMORY_SURVEY_EVENT, {
- event_type: 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
+ event_type:
+ 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ appearance_id:
+ appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
void logOTelEvent('feedback_survey', {
event_type: 'appeared',
appearance_id: appearanceId,
- survey_type: 'memory'
- });
- }, []);
- const onSelect = useCallback((appearanceId_0: string, selected: FeedbackSurveyResponse) => {
+ survey_type: 'memory',
+ })
+ }, [])
+
+ const onSelect = useCallback(
+ (appearanceId: string, selected: FeedbackSurveyResponse) => {
+ logEvent(MEMORY_SURVEY_EVENT, {
+ event_type:
+ 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ appearance_id:
+ appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ response:
+ selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
+ void logOTelEvent('feedback_survey', {
+ event_type: 'responded',
+ appearance_id: appearanceId,
+ response: selected,
+ survey_type: 'memory',
+ })
+ },
+ [],
+ )
+
+ const shouldShowTranscriptPrompt = useCallback(
+ (selected: FeedbackSurveyResponse) => {
+ if (process.env.USER_TYPE !== 'ant') {
+ return false
+ }
+ if (selected !== 'bad' && selected !== 'good') {
+ return false
+ }
+ if (getGlobalConfig().transcriptShareDismissed) {
+ return false
+ }
+ if (!isPolicyAllowed('allow_product_feedback')) {
+ return false
+ }
+ return true
+ },
+ [],
+ )
+
+ const onTranscriptPromptShown = useCallback((appearanceId: string) => {
logEvent(MEMORY_SURVEY_EVENT, {
- event_type: 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
- void logOTelEvent('feedback_survey', {
- event_type: 'responded',
- appearance_id: appearanceId_0,
- response: selected,
- survey_type: 'memory'
- });
- }, []);
- const shouldShowTranscriptPrompt = useCallback((selected_0: FeedbackSurveyResponse) => {
- if ((process.env.USER_TYPE) !== 'ant') {
- return false;
- }
- if (selected_0 !== 'bad' && selected_0 !== 'good') {
- return false;
- }
- if (getGlobalConfig().transcriptShareDismissed) {
- return false;
- }
- if (!isPolicyAllowed('allow_product_feedback')) {
- return false;
- }
- return true;
- }, []);
- const onTranscriptPromptShown = useCallback((appearanceId_1: string) => {
- logEvent(MEMORY_SURVEY_EVENT, {
- event_type: 'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- appearance_id: appearanceId_1 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
+ event_type:
+ 'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ appearance_id:
+ appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ trigger:
+ TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
void logOTelEvent('feedback_survey', {
event_type: 'transcript_prompt_appeared',
- appearance_id: appearanceId_1,
- survey_type: 'memory'
- });
- }, []);
- const onTranscriptSelect = useCallback(async (appearanceId_2: string, selected_1: TranscriptShareResponse): Promise => {
- logEvent(MEMORY_SURVEY_EVENT, {
- event_type: `transcript_share_${selected_1}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
- if (selected_1 === 'dont_ask_again') {
- saveGlobalConfig(current => ({
- ...current,
- transcriptShareDismissed: true
- }));
- }
- if (selected_1 === 'yes') {
- const result = await submitTranscriptShare(messagesRef.current, TRANSCRIPT_SHARE_TRIGGER, appearanceId_2);
+ appearance_id: appearanceId,
+ survey_type: 'memory',
+ })
+ }, [])
+
+ const onTranscriptSelect = useCallback(
+ async (
+ appearanceId: string,
+ selected: TranscriptShareResponse,
+ ): Promise => {
logEvent(MEMORY_SURVEY_EVENT, {
- event_type: (result.success ? 'transcript_share_submitted' : 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
- return result.success;
- }
- return false;
- }, []);
- const {
- state,
- lastResponse,
- open,
- handleSelect,
- handleTranscriptSelect
- } = useSurveyState({
- hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
- onOpen,
- onSelect,
- shouldShowTranscriptPrompt,
- onTranscriptPromptShown,
- onTranscriptSelect
- });
- const lastAssistant = useMemo(() => getLastAssistantMessage(messages), [messages]);
+ event_type:
+ `transcript_share_${selected}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ appearance_id:
+ appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ trigger:
+ TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
+
+ if (selected === 'dont_ask_again') {
+ saveGlobalConfig(current => ({
+ ...current,
+ transcriptShareDismissed: true,
+ }))
+ }
+
+ if (selected === 'yes') {
+ const result = await submitTranscriptShare(
+ messagesRef.current,
+ TRANSCRIPT_SHARE_TRIGGER,
+ appearanceId,
+ )
+ logEvent(MEMORY_SURVEY_EVENT, {
+ event_type: (result.success
+ ? 'transcript_share_submitted'
+ : 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ appearance_id:
+ appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ trigger:
+ TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
+ return result.success
+ }
+
+ return false
+ },
+ [],
+ )
+
+ const { state, lastResponse, open, handleSelect, handleTranscriptSelect } =
+ useSurveyState({
+ hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
+ onOpen,
+ onSelect,
+ shouldShowTranscriptPrompt,
+ onTranscriptPromptShown,
+ onTranscriptSelect,
+ })
+
+ const lastAssistant = useMemo(
+ () => getLastAssistantMessage(messages),
+ [messages],
+ )
+
useEffect(() => {
- if (!enabled) return;
+ if (!enabled) return
// /clear resets messages but REPL stays mounted — reset refs so a memory
// read from the previous conversation doesn't leak into the new one.
if (messages.length === 0) {
- memoryReadSeen.current = false;
- seenAssistantUuids.current.clear();
- return;
+ memoryReadSeen.current = false
+ seenAssistantUuids.current.clear()
+ return
}
+
if (state !== 'closed' || isLoading || hasActivePrompt) {
- return;
+ return
}
// 3P default: survey off (no GrowthBook on Bedrock/Vertex/Foundry).
if (!getFeatureValue_CACHED_MAY_BE_STALE(MEMORY_SURVEY_GATE, false)) {
- return;
+ return
}
+
if (!isAutoMemoryEnabled()) {
- return;
+ return
}
+
if (isFeedbackSurveyDisabled()) {
- return;
+ return
}
+
if (!isPolicyAllowed('allow_product_feedback')) {
- return;
+ return
}
+
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
- return;
+ return
}
+
if (!lastAssistant || seenAssistantUuids.current.has(lastAssistant.uuid)) {
- return;
+ return
}
- const text = extractTextContent(Array.isArray(lastAssistant.message.content) ? lastAssistant.message.content : [], ' ');
+
+ const text = extractTextContent(lastAssistant.message.content, ' ')
if (!MEMORY_WORD_RE.test(text)) {
- return;
+ return
}
// Mark as evaluated before the memory-read scan so a turn that mentions
// "memory" but has no memory read doesn't trigger repeated O(n) scans
// on subsequent renders with the same last assistant message.
- seenAssistantUuids.current.add(lastAssistant.uuid);
+ seenAssistantUuids.current.add(lastAssistant.uuid)
+
if (!memoryReadSeen.current) {
- memoryReadSeen.current = hasMemoryFileRead(messages);
+ memoryReadSeen.current = hasMemoryFileRead(messages)
}
if (!memoryReadSeen.current) {
- return;
+ return
}
+
if (Math.random() < SURVEY_PROBABILITY) {
- open();
+ open()
}
- }, [enabled, state, isLoading, hasActivePrompt, lastAssistant, messages, open]);
- return {
+ }, [
+ enabled,
state,
- lastResponse,
- handleSelect,
- handleTranscriptSelect
- };
+ isLoading,
+ hasActivePrompt,
+ lastAssistant,
+ messages,
+ open,
+ ])
+
+ return { state, lastResponse, handleSelect, handleTranscriptSelect }
}
diff --git a/src/components/FeedbackSurvey/usePostCompactSurvey.tsx b/src/components/FeedbackSurvey/usePostCompactSurvey.tsx
index ee8e31e79..99e80dfed 100644
--- a/src/components/FeedbackSurvey/usePostCompactSurvey.tsx
+++ b/src/components/FeedbackSurvey/usePostCompactSurvey.tsx
@@ -1,205 +1,195 @@
-import { c as _c } from "react/compiler-runtime";
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js';
-import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js';
-import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
-import { shouldUseSessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js';
-import type { Message } from '../../types/message.js';
-import { isEnvTruthy } from '../../utils/envUtils.js';
-import { isCompactBoundaryMessage } from '../../utils/messages.js';
-import { logOTelEvent } from '../../utils/telemetry/events.js';
-import { useSurveyState } from './useSurveyState.js';
-import type { FeedbackSurveyResponse } from './utils.js';
-const HIDE_THANKS_AFTER_MS = 3000;
-const POST_COMPACT_SURVEY_GATE = 'tengu_post_compact_survey';
-const SURVEY_PROBABILITY = 0.2; // Show survey 20% of the time after compaction
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'
+import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
+import {
+ type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ logEvent,
+} from 'src/services/analytics/index.js'
+import { shouldUseSessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js'
+import type { Message } from '../../types/message.js'
+import { isEnvTruthy } from '../../utils/envUtils.js'
+import { isCompactBoundaryMessage } from '../../utils/messages.js'
+import { logOTelEvent } from '../../utils/telemetry/events.js'
+import { useSurveyState } from './useSurveyState.js'
+import type { FeedbackSurveyResponse } from './utils.js'
-function hasMessageAfterBoundary(messages: Message[], boundaryUuid: string): boolean {
- const boundaryIndex = messages.findIndex(msg => msg.uuid === boundaryUuid);
+const HIDE_THANKS_AFTER_MS = 3000
+const POST_COMPACT_SURVEY_GATE = 'tengu_post_compact_survey'
+const SURVEY_PROBABILITY = 0.2 // Show survey 20% of the time after compaction
+
+function hasMessageAfterBoundary(
+ messages: Message[],
+ boundaryUuid: string,
+): boolean {
+ const boundaryIndex = messages.findIndex(msg => msg.uuid === boundaryUuid)
if (boundaryIndex === -1) {
- return false;
+ return false
}
// Check if there's a user or assistant message after the boundary
for (let i = boundaryIndex + 1; i < messages.length; i++) {
- const msg = messages[i];
+ const msg = messages[i]
if (msg && (msg.type === 'user' || msg.type === 'assistant')) {
- return true;
+ return true
}
}
- return false;
+ return false
}
-export function usePostCompactSurvey(messages, isLoading, t0, t1) {
- const $ = _c(23);
- const hasActivePrompt = t0 === undefined ? false : t0;
- let t2;
- if ($[0] !== t1) {
- t2 = t1 === undefined ? {} : t1;
- $[0] = t1;
- $[1] = t2;
- } else {
- t2 = $[1];
- }
- const {
- enabled: t3
- } = t2;
- const enabled = t3 === undefined ? true : t3;
- const [gateEnabled, setGateEnabled] = useState(null);
- let t4;
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
- t4 = new Set();
- $[2] = t4;
- } else {
- t4 = $[2];
- }
- const seenCompactBoundaries = useRef(t4);
- const pendingCompactBoundaryUuid = useRef(null);
- const onOpen = _temp;
- const onSelect = _temp2;
- let t5;
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
- t5 = {
- hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
- onOpen,
- onSelect
- };
- $[3] = t5;
- } else {
- t5 = $[3];
- }
- const {
- state,
- lastResponse,
- open,
- handleSelect
- } = useSurveyState(t5);
- let t6;
- let t7;
- if ($[4] !== enabled) {
- t6 = () => {
- if (!enabled) {
- return;
- }
- setGateEnabled(checkStatsigFeatureGate_CACHED_MAY_BE_STALE(POST_COMPACT_SURVEY_GATE));
- };
- t7 = [enabled];
- $[4] = enabled;
- $[5] = t6;
- $[6] = t7;
- } else {
- t6 = $[5];
- t7 = $[6];
- }
- useEffect(t6, t7);
- let t8;
- if ($[7] !== messages) {
- t8 = new Set(messages.filter(_temp3).map(_temp4));
- $[7] = messages;
- $[8] = t8;
- } else {
- t8 = $[8];
- }
- const currentCompactBoundaries = t8;
- let t10;
- let t9;
- if ($[9] !== currentCompactBoundaries || $[10] !== enabled || $[11] !== gateEnabled || $[12] !== hasActivePrompt || $[13] !== isLoading || $[14] !== messages || $[15] !== open || $[16] !== state) {
- t9 = () => {
- if (!enabled) {
- return;
- }
- if (state !== "closed" || isLoading) {
- return;
- }
- if (hasActivePrompt) {
- return;
- }
- if (gateEnabled !== true) {
- return;
- }
- if (isFeedbackSurveyDisabled()) {
- return;
- }
- if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
- return;
- }
- if (pendingCompactBoundaryUuid.current !== null) {
- if (hasMessageAfterBoundary(messages, pendingCompactBoundaryUuid.current)) {
- pendingCompactBoundaryUuid.current = null;
- if (Math.random() < SURVEY_PROBABILITY) {
- open();
- }
- return;
+
+export function usePostCompactSurvey(
+ messages: Message[],
+ isLoading: boolean,
+ hasActivePrompt = false,
+ { enabled = true }: { enabled?: boolean } = {},
+): {
+ state:
+ | 'closed'
+ | 'open'
+ | 'thanks'
+ | 'transcript_prompt'
+ | 'submitting'
+ | 'submitted'
+ lastResponse: FeedbackSurveyResponse | null
+ handleSelect: (selected: FeedbackSurveyResponse) => void
+} {
+ const [gateEnabled, setGateEnabled] = useState(null)
+ const seenCompactBoundaries = useRef>(new Set())
+ // Track the compact boundary we're waiting on (to show survey after next message)
+ const pendingCompactBoundaryUuid = useRef(null)
+
+ const onOpen = useCallback((appearanceId: string) => {
+ const smCompactionEnabled = shouldUseSessionMemoryCompaction()
+ logEvent('tengu_post_compact_survey_event', {
+ event_type:
+ 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ appearance_id:
+ appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ session_memory_compaction_enabled:
+ smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
+ void logOTelEvent('feedback_survey', {
+ event_type: 'appeared',
+ appearance_id: appearanceId,
+ survey_type: 'post_compact',
+ })
+ }, [])
+
+ const onSelect = useCallback(
+ (appearanceId: string, selected: FeedbackSurveyResponse) => {
+ const smCompactionEnabled = shouldUseSessionMemoryCompaction()
+ logEvent('tengu_post_compact_survey_event', {
+ event_type:
+ 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ appearance_id:
+ appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ response:
+ selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ session_memory_compaction_enabled:
+ smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
+ void logOTelEvent('feedback_survey', {
+ event_type: 'responded',
+ appearance_id: appearanceId,
+ response: selected,
+ survey_type: 'post_compact',
+ })
+ },
+ [],
+ )
+
+ const { state, lastResponse, open, handleSelect } = useSurveyState({
+ hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
+ onOpen,
+ onSelect,
+ })
+
+ // Check the feature gate on mount
+ useEffect(() => {
+ if (!enabled) return
+ setGateEnabled(
+ checkStatsigFeatureGate_CACHED_MAY_BE_STALE(POST_COMPACT_SURVEY_GATE),
+ )
+ }, [enabled])
+
+ // Find compact boundary messages
+ const currentCompactBoundaries = useMemo(
+ () =>
+ new Set(
+ messages
+ .filter(msg => isCompactBoundaryMessage(msg))
+ .map(msg => msg.uuid),
+ ),
+ [messages],
+ )
+
+ // Detect new compact boundaries and defer showing survey until next message
+ useEffect(() => {
+ if (!enabled) return
+
+ // Don't process if already showing
+ if (state !== 'closed' || isLoading) {
+ return
+ }
+
+ // Don't show survey when permission or ask question prompts are visible
+ if (hasActivePrompt) {
+ return
+ }
+
+ // Check if the gate is enabled
+ if (gateEnabled !== true) {
+ return
+ }
+
+ if (isFeedbackSurveyDisabled()) {
+ return
+ }
+
+ // Check if survey is explicitly disabled
+ if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
+ return
+ }
+
+ // First, check if we have a pending compact and a new message has arrived
+ if (pendingCompactBoundaryUuid.current !== null) {
+ if (
+ hasMessageAfterBoundary(messages, pendingCompactBoundaryUuid.current)
+ ) {
+ // A new message arrived after the compact - decide whether to show survey
+ pendingCompactBoundaryUuid.current = null
+
+ // Only show survey 20% of the time
+ if (Math.random() < SURVEY_PROBABILITY) {
+ open()
}
+ return
}
- const newBoundaries = Array.from(currentCompactBoundaries).filter(uuid => !seenCompactBoundaries.current.has(uuid));
- if (newBoundaries.length > 0) {
- seenCompactBoundaries.current = new Set(currentCompactBoundaries);
- pendingCompactBoundaryUuid.current = newBoundaries[newBoundaries.length - 1];
- }
- };
- t10 = [enabled, currentCompactBoundaries, state, isLoading, hasActivePrompt, gateEnabled, messages, open];
- $[9] = currentCompactBoundaries;
- $[10] = enabled;
- $[11] = gateEnabled;
- $[12] = hasActivePrompt;
- $[13] = isLoading;
- $[14] = messages;
- $[15] = open;
- $[16] = state;
- $[17] = t10;
- $[18] = t9;
- } else {
- t10 = $[17];
- t9 = $[18];
- }
- useEffect(t9, t10);
- let t11;
- if ($[19] !== handleSelect || $[20] !== lastResponse || $[21] !== state) {
- t11 = {
- state,
- lastResponse,
- handleSelect
- };
- $[19] = handleSelect;
- $[20] = lastResponse;
- $[21] = state;
- $[22] = t11;
- } else {
- t11 = $[22];
- }
- return t11;
-}
-function _temp4(msg_0) {
- return msg_0.uuid;
-}
-function _temp3(msg) {
- return isCompactBoundaryMessage(msg);
-}
-function _temp2(appearanceId_0, selected) {
- const smCompactionEnabled_0 = shouldUseSessionMemoryCompaction();
- logEvent("tengu_post_compact_survey_event", {
- event_type: "responded" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- session_memory_compaction_enabled: smCompactionEnabled_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
- logOTelEvent("feedback_survey", {
- event_type: "responded",
- appearance_id: appearanceId_0,
- response: selected,
- survey_type: "post_compact"
- });
-}
-function _temp(appearanceId) {
- const smCompactionEnabled = shouldUseSessionMemoryCompaction();
- logEvent("tengu_post_compact_survey_event", {
- event_type: "appeared" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- session_memory_compaction_enabled: smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
- logOTelEvent("feedback_survey", {
- event_type: "appeared",
- appearance_id: appearanceId,
- survey_type: "post_compact"
- });
+ }
+
+ // Find new compact boundaries that we haven't seen yet
+ const newBoundaries = Array.from(currentCompactBoundaries).filter(
+ uuid => !seenCompactBoundaries.current.has(uuid),
+ )
+
+ if (newBoundaries.length > 0) {
+ // Mark these boundaries as seen
+ seenCompactBoundaries.current = new Set(currentCompactBoundaries)
+
+ // Don't show survey immediately - wait for next message
+ // Store the most recent new boundary UUID
+ pendingCompactBoundaryUuid.current =
+ newBoundaries[newBoundaries.length - 1]!
+ }
+ }, [
+ enabled,
+ currentCompactBoundaries,
+ state,
+ isLoading,
+ hasActivePrompt,
+ gateEnabled,
+ messages,
+ open,
+ ])
+
+ return { state, lastResponse, handleSelect }
}
diff --git a/src/components/FeedbackSurvey/useSurveyState.tsx b/src/components/FeedbackSurvey/useSurveyState.tsx
index d98e6d655..e00c82c0d 100644
--- a/src/components/FeedbackSurvey/useSurveyState.tsx
+++ b/src/components/FeedbackSurvey/useSurveyState.tsx
@@ -1,99 +1,144 @@
-import { randomUUID } from 'crypto';
-import { useCallback, useRef, useState } from 'react';
-import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
-import type { FeedbackSurveyResponse } from './utils.js';
-type SurveyState = 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
+import { randomUUID } from 'crypto'
+import { useCallback, useRef, useState } from 'react'
+import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
+import type { FeedbackSurveyResponse } from './utils.js'
+
+type SurveyState =
+ | 'closed'
+ | 'open'
+ | 'thanks'
+ | 'transcript_prompt'
+ | 'submitting'
+ | 'submitted'
+
type UseSurveyStateOptions = {
- hideThanksAfterMs: number;
- onOpen: (appearanceId: string) => void | Promise;
- onSelect: (appearanceId: string, selected: FeedbackSurveyResponse) => void | Promise;
- shouldShowTranscriptPrompt?: (selected: FeedbackSurveyResponse) => boolean;
- onTranscriptPromptShown?: (appearanceId: string, surveyResponse: FeedbackSurveyResponse) => void;
- onTranscriptSelect?: (appearanceId: string, selected: TranscriptShareResponse, surveyResponse: FeedbackSurveyResponse | null) => boolean | Promise;
-};
+ hideThanksAfterMs: number
+ onOpen: (appearanceId: string) => void | Promise
+ onSelect: (
+ appearanceId: string,
+ selected: FeedbackSurveyResponse,
+ ) => void | Promise
+ shouldShowTranscriptPrompt?: (selected: FeedbackSurveyResponse) => boolean
+ onTranscriptPromptShown?: (
+ appearanceId: string,
+ surveyResponse: FeedbackSurveyResponse,
+ ) => void
+ onTranscriptSelect?: (
+ appearanceId: string,
+ selected: TranscriptShareResponse,
+ surveyResponse: FeedbackSurveyResponse | null,
+ ) => boolean | Promise
+}
+
export function useSurveyState({
hideThanksAfterMs,
onOpen,
onSelect,
shouldShowTranscriptPrompt,
onTranscriptPromptShown,
- onTranscriptSelect
+ onTranscriptSelect,
}: UseSurveyStateOptions): {
- state: SurveyState;
- lastResponse: FeedbackSurveyResponse | null;
- open: () => void;
- handleSelect: (selected: FeedbackSurveyResponse) => boolean;
- handleTranscriptSelect: (selected: TranscriptShareResponse) => void;
+ state: SurveyState
+ lastResponse: FeedbackSurveyResponse | null
+ open: () => void
+ handleSelect: (selected: FeedbackSurveyResponse) => boolean
+ handleTranscriptSelect: (selected: TranscriptShareResponse) => void
} {
- const [state, setState] = useState('closed');
- const [lastResponse, setLastResponse] = useState(null);
- const appearanceId = useRef(randomUUID());
- const lastResponseRef = useRef(null);
+ const [state, setState] = useState('closed')
+ const [lastResponse, setLastResponse] =
+ useState(null)
+ const appearanceId = useRef(randomUUID())
+ const lastResponseRef = useRef(null)
+
const showThanksThenClose = useCallback(() => {
- setState('thanks');
- setTimeout((setState_0, setLastResponse_0) => {
- setState_0('closed');
- setLastResponse_0(null);
- }, hideThanksAfterMs, setState, setLastResponse);
- }, [hideThanksAfterMs]);
+ setState('thanks')
+ setTimeout(
+ (setState, setLastResponse) => {
+ setState('closed')
+ setLastResponse(null)
+ },
+ hideThanksAfterMs,
+ setState,
+ setLastResponse,
+ )
+ }, [hideThanksAfterMs])
+
const showSubmittedThenClose = useCallback(() => {
- setState('submitted');
- setTimeout(setState, hideThanksAfterMs, 'closed');
- }, [hideThanksAfterMs]);
+ setState('submitted')
+ setTimeout(setState, hideThanksAfterMs, 'closed')
+ }, [hideThanksAfterMs])
+
const open = useCallback(() => {
if (state !== 'closed') {
- return;
+ return
}
- setState('open');
- appearanceId.current = randomUUID();
- void onOpen(appearanceId.current);
- }, [state, onOpen]);
- const handleSelect = useCallback((selected: FeedbackSurveyResponse): boolean => {
- setLastResponse(selected);
- lastResponseRef.current = selected;
- // Always fire the survey response event first
- void onSelect(appearanceId.current, selected);
- if (selected === 'dismissed') {
- setState('closed');
- setLastResponse(null);
- } else if (shouldShowTranscriptPrompt?.(selected)) {
- setState('transcript_prompt');
- onTranscriptPromptShown?.(appearanceId.current, selected);
- return true;
- } else {
- showThanksThenClose();
- }
- return false;
- }, [showThanksThenClose, onSelect, shouldShowTranscriptPrompt, onTranscriptPromptShown]);
- const handleTranscriptSelect = useCallback((selected_0: TranscriptShareResponse) => {
- switch (selected_0) {
- case 'yes':
- setState('submitting');
- void (async () => {
- try {
- const success = await onTranscriptSelect?.(appearanceId.current, selected_0, lastResponseRef.current);
- if (success) {
- showSubmittedThenClose();
- } else {
- showThanksThenClose();
+ setState('open')
+ appearanceId.current = randomUUID()
+ void onOpen(appearanceId.current)
+ }, [state, onOpen])
+
+ const handleSelect = useCallback(
+ (selected: FeedbackSurveyResponse): boolean => {
+ setLastResponse(selected)
+ lastResponseRef.current = selected
+ // Always fire the survey response event first
+ void onSelect(appearanceId.current, selected)
+
+ if (selected === 'dismissed') {
+ setState('closed')
+ setLastResponse(null)
+ } else if (shouldShowTranscriptPrompt?.(selected)) {
+ setState('transcript_prompt')
+ onTranscriptPromptShown?.(appearanceId.current, selected)
+ return true
+ } else {
+ showThanksThenClose()
+ }
+ return false
+ },
+ [
+ showThanksThenClose,
+ onSelect,
+ shouldShowTranscriptPrompt,
+ onTranscriptPromptShown,
+ ],
+ )
+
+ const handleTranscriptSelect = useCallback(
+ (selected: TranscriptShareResponse) => {
+ switch (selected) {
+ case 'yes':
+ setState('submitting')
+ void (async () => {
+ try {
+ const success = await onTranscriptSelect?.(
+ appearanceId.current,
+ selected,
+ lastResponseRef.current,
+ )
+ if (success) {
+ showSubmittedThenClose()
+ } else {
+ showThanksThenClose()
+ }
+ } catch {
+ showThanksThenClose()
}
- } catch {
- showThanksThenClose();
- }
- })();
- break;
- case 'no':
- case 'dont_ask_again':
- void onTranscriptSelect?.(appearanceId.current, selected_0, lastResponseRef.current);
- showThanksThenClose();
- break;
- }
- }, [showThanksThenClose, showSubmittedThenClose, onTranscriptSelect]);
- return {
- state,
- lastResponse,
- open,
- handleSelect,
- handleTranscriptSelect
- };
+ })()
+ break
+ case 'no':
+ case 'dont_ask_again':
+ void onTranscriptSelect?.(
+ appearanceId.current,
+ selected,
+ lastResponseRef.current,
+ )
+ showThanksThenClose()
+ break
+ }
+ },
+ [showThanksThenClose, showSubmittedThenClose, onTranscriptSelect],
+ )
+
+ return { state, lastResponse, open, handleSelect, handleTranscriptSelect }
}
diff --git a/src/components/PromptInput/HistorySearchInput.tsx b/src/components/PromptInput/HistorySearchInput.tsx
index dba1f4cda..22830119d 100644
--- a/src/components/PromptInput/HistorySearchInput.tsx
+++ b/src/components/PromptInput/HistorySearchInput.tsx
@@ -1,50 +1,38 @@
-import { c as _c } from "react/compiler-runtime";
-import * as React from 'react';
-import { stringWidth } from '../../ink/stringWidth.js';
-import { Box, Text } from '../../ink.js';
-import TextInput from '../TextInput.js';
+import * as React from 'react'
+import { stringWidth } from '../../ink/stringWidth.js'
+import { Box, Text } from '../../ink.js'
+import TextInput from '../TextInput.js'
+
type Props = {
- value: string;
- onChange: (value: string) => void;
- historyFailedMatch: boolean;
-};
-function HistorySearchInput(t0) {
- const $ = _c(9);
- const {
- value,
- onChange,
- historyFailedMatch
- } = t0;
- const t1 = historyFailedMatch ? "no matching prompt:" : "search prompts:";
- let t2;
- if ($[0] !== t1) {
- t2 = {t1};
- $[0] = t1;
- $[1] = t2;
- } else {
- t2 = $[1];
- }
- const t3 = stringWidth(value) + 1;
- let t4;
- if ($[2] !== onChange || $[3] !== t3 || $[4] !== value) {
- t4 = ;
- $[2] = onChange;
- $[3] = t3;
- $[4] = value;
- $[5] = t4;
- } else {
- t4 = $[5];
- }
- let t5;
- if ($[6] !== t2 || $[7] !== t4) {
- t5 = {t2}{t4};
- $[6] = t2;
- $[7] = t4;
- $[8] = t5;
- } else {
- t5 = $[8];
- }
- return t5;
+ value: string
+ onChange: (value: string) => void
+ historyFailedMatch: boolean
}
-function _temp() {}
-export default HistorySearchInput;
+
+function HistorySearchInput({
+ value,
+ onChange,
+ historyFailedMatch,
+}: Props): React.ReactNode {
+ return (
+
+
+ {historyFailedMatch ? 'no matching prompt:' : 'search prompts:'}
+
+ {}}
+ columns={stringWidth(value) + 1}
+ focus={true}
+ showCursor={true}
+ multiline={false}
+ dimColor={true}
+ />
+
+ )
+}
+
+export default HistorySearchInput
diff --git a/src/components/PromptInput/IssueFlagBanner.tsx b/src/components/PromptInput/IssueFlagBanner.tsx
index bb5d6b5d8..723678eaf 100644
--- a/src/components/PromptInput/IssueFlagBanner.tsx
+++ b/src/components/PromptInput/IssueFlagBanner.tsx
@@ -1,11 +1,28 @@
-import * as React from 'react';
-import { FLAG_ICON } from '../../constants/figures.js';
-import { Box, Text } from '../../ink.js';
+import * as React from 'react'
+import { FLAG_ICON } from '../../constants/figures.js'
+import { Box, Text } from '../../ink.js'
/**
* ANT-ONLY: Banner shown in the transcript that prompts users to report
* issues via /issue. Appears when friction is detected in the conversation.
*/
-export function IssueFlagBanner() {
- return null;
+export function IssueFlagBanner(): React.ReactNode {
+ if (process.env.USER_TYPE !== 'ant') {
+ return null
+ }
+
+ return (
+
+
+ {FLAG_ICON}
+
+
+ [ANT-ONLY]
+
+ Something off with Claude?
+
+ /issue to report it
+
+
+ )
}
diff --git a/src/components/PromptInput/Notifications.tsx b/src/components/PromptInput/Notifications.tsx
index 5203318e3..d89d596b3 100644
--- a/src/components/PromptInput/Notifications.tsx
+++ b/src/components/PromptInput/Notifications.tsx
@@ -1,218 +1,201 @@
-import { c as _c } from "react/compiler-runtime";
-import { feature } from 'bun:bundle';
-import * as React from 'react';
-import { type ReactNode, useEffect, useMemo, useState } from 'react';
-import { type Notification, useNotifications } from 'src/context/notifications.js';
-import { logEvent } from 'src/services/analytics/index.js';
-import { useAppState } from 'src/state/AppState.js';
-import { useVoiceState } from '../../context/voice.js';
-import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js';
-import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js';
-import type { IDESelection } from '../../hooks/useIdeSelection.js';
-import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
-import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js';
-import { Box, Text } from '../../ink.js';
-import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js';
-import { calculateTokenWarningState } from '../../services/compact/autoCompact.js';
-import type { MCPServerConnection } from '../../services/mcp/types.js';
-import type { Message } from '../../types/message.js';
-import { getApiKeyHelperElapsedMs, getConfiguredApiKeyHelper, getSubscriptionType } from '../../utils/auth.js';
-import type { AutoUpdaterResult } from '../../utils/autoUpdater.js';
-import { getExternalEditor } from '../../utils/editor.js';
-import { isEnvTruthy } from '../../utils/envUtils.js';
-import { formatDuration } from '../../utils/format.js';
-import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js';
-import { toIDEDisplayName } from '../../utils/ide.js';
-import { getMessagesAfterCompactBoundary } from '../../utils/messages.js';
-import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js';
-import { AutoUpdaterWrapper } from '../AutoUpdaterWrapper.js';
-import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
-import { IdeStatusIndicator } from '../IdeStatusIndicator.js';
-import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js';
-import { SentryErrorBoundary } from '../SentryErrorBoundary.js';
-import { TokenWarning } from '../TokenWarning.js';
-import { SandboxPromptFooterHint } from './SandboxPromptFooterHint.js';
+import { feature } from 'bun:bundle'
+import * as React from 'react'
+import { type ReactNode, useEffect, useMemo, useState } from 'react'
+import {
+ type Notification,
+ useNotifications,
+} from 'src/context/notifications.js'
+import { logEvent } from 'src/services/analytics/index.js'
+import { useAppState } from 'src/state/AppState.js'
+import { useVoiceState } from '../../context/voice.js'
+import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
+import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js'
+import type { IDESelection } from '../../hooks/useIdeSelection.js'
+import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
+import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'
+import { Box, Text } from '../../ink.js'
+import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'
+import { calculateTokenWarningState } from '../../services/compact/autoCompact.js'
+import type { MCPServerConnection } from '../../services/mcp/types.js'
+import type { Message } from '../../types/message.js'
+import {
+ getApiKeyHelperElapsedMs,
+ getConfiguredApiKeyHelper,
+ getSubscriptionType,
+} from '../../utils/auth.js'
+import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
+import { getExternalEditor } from '../../utils/editor.js'
+import { isEnvTruthy } from '../../utils/envUtils.js'
+import { formatDuration } from '../../utils/format.js'
+import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js'
+import { toIDEDisplayName } from '../../utils/ide.js'
+import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'
+import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js'
+import { AutoUpdaterWrapper } from '../AutoUpdaterWrapper.js'
+import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
+import { IdeStatusIndicator } from '../IdeStatusIndicator.js'
+import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js'
+import { SentryErrorBoundary } from '../SentryErrorBoundary.js'
+import { TokenWarning } from '../TokenWarning.js'
+import { SandboxPromptFooterHint } from './SandboxPromptFooterHint.js'
/* eslint-disable @typescript-eslint/no-require-imports */
-const VoiceIndicator: typeof import('./VoiceIndicator.js').VoiceIndicator = feature('VOICE_MODE') ? require('./VoiceIndicator.js').VoiceIndicator : () => null;
+const VoiceIndicator: typeof import('./VoiceIndicator.js').VoiceIndicator =
+ feature('VOICE_MODE')
+ ? require('./VoiceIndicator.js').VoiceIndicator
+ : () => null
/* eslint-enable @typescript-eslint/no-require-imports */
-export const FOOTER_TEMPORARY_STATUS_TIMEOUT = 5000;
+export const FOOTER_TEMPORARY_STATUS_TIMEOUT = 5000
+
type Props = {
- apiKeyStatus: VerificationStatus;
- autoUpdaterResult: AutoUpdaterResult | null;
- isAutoUpdating: boolean;
- debug: boolean;
- verbose: boolean;
- messages: Message[];
- onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
- onChangeIsUpdating: (isUpdating: boolean) => void;
- ideSelection: IDESelection | undefined;
- mcpClients?: MCPServerConnection[];
- isInputWrapped?: boolean;
- isNarrow?: boolean;
-};
-export function Notifications(t0) {
- const $ = _c(34);
- const {
- apiKeyStatus,
- autoUpdaterResult,
- debug,
- isAutoUpdating,
- verbose,
- messages,
- onAutoUpdaterResult,
- onChangeIsUpdating,
- ideSelection,
- mcpClients,
- isInputWrapped: t1,
- isNarrow: t2
- } = t0;
- const isInputWrapped = t1 === undefined ? false : t1;
- const isNarrow = t2 === undefined ? false : t2;
- let t3;
- if ($[0] !== messages) {
- const messagesForTokenCount = getMessagesAfterCompactBoundary(messages);
- t3 = tokenCountFromLastAPIResponse(messagesForTokenCount);
- $[0] = messages;
- $[1] = t3;
- } else {
- t3 = $[1];
- }
- const tokenUsage = t3;
- const mainLoopModel = useMainLoopModel();
- let t4;
- if ($[2] !== mainLoopModel || $[3] !== tokenUsage) {
- t4 = calculateTokenWarningState(tokenUsage, mainLoopModel);
- $[2] = mainLoopModel;
- $[3] = tokenUsage;
- $[4] = t4;
- } else {
- t4 = $[4];
- }
- const isShowingCompactMessage = t4.isAboveWarningThreshold;
- const {
- status: ideStatus
- } = useIdeConnectionStatus(mcpClients);
- const notifications = useAppState(_temp);
- const {
+ apiKeyStatus: VerificationStatus
+ autoUpdaterResult: AutoUpdaterResult | null
+ isAutoUpdating: boolean
+ debug: boolean
+ verbose: boolean
+ messages: Message[]
+ onAutoUpdaterResult: (result: AutoUpdaterResult) => void
+ onChangeIsUpdating: (isUpdating: boolean) => void
+ ideSelection: IDESelection | undefined
+ mcpClients?: MCPServerConnection[]
+ isInputWrapped?: boolean
+ isNarrow?: boolean
+}
+
+export function Notifications({
+ apiKeyStatus,
+ autoUpdaterResult,
+ debug,
+ isAutoUpdating,
+ verbose,
+ messages,
+ onAutoUpdaterResult,
+ onChangeIsUpdating,
+ ideSelection,
+ mcpClients,
+ isInputWrapped = false,
+ isNarrow = false,
+}: Props): ReactNode {
+ const tokenUsage = useMemo(() => {
+ const messagesForTokenCount = getMessagesAfterCompactBoundary(messages)
+ return tokenCountFromLastAPIResponse(messagesForTokenCount)
+ }, [messages])
+
+ // AppState-sourced model — same source as API requests. getMainLoopModel()
+ // re-reads settings.json on every call, so another session's /model write
+ // would leak into this session's display (anthropics/claude-code#37596).
+ const mainLoopModel = useMainLoopModel()
+ const isShowingCompactMessage = calculateTokenWarningState(
+ tokenUsage,
+ mainLoopModel,
+ ).isAboveWarningThreshold
+ const { status: ideStatus } = useIdeConnectionStatus(mcpClients)
+ const notifications = useAppState(s => s.notifications)
+ const { addNotification, removeNotification } = useNotifications()
+ const claudeAiLimits = useClaudeAiLimits()
+
+ // Register env hook notifier for CwdChanged/FileChanged feedback
+ useEffect(() => {
+ setEnvHookNotifier((text, isError) => {
+ addNotification({
+ key: 'env-hook',
+ text,
+ color: isError ? 'error' : undefined,
+ priority: isError ? 'medium' : 'low',
+ timeoutMs: isError ? 8000 : 5000,
+ })
+ })
+ return () => setEnvHookNotifier(null)
+ }, [addNotification])
+
+ // Check if we should show the IDE selection indicator
+ const shouldShowIdeSelection =
+ ideStatus === 'connected' &&
+ (ideSelection?.filePath ||
+ (ideSelection?.text && ideSelection.lineCount > 0))
+
+ // Hide update installed message when showing IDE selection
+ const shouldShowAutoUpdater =
+ !shouldShowIdeSelection ||
+ isAutoUpdating ||
+ autoUpdaterResult?.status !== 'success'
+
+ // Check if we're in overage mode for UI indicators
+ const isInOverageMode = claudeAiLimits.isUsingOverage
+ const subscriptionType = getSubscriptionType()
+ const isTeamOrEnterprise =
+ subscriptionType === 'team' || subscriptionType === 'enterprise'
+
+ // Check if the external editor hint should be shown
+ const editor = getExternalEditor()
+ const shouldShowExternalEditorHint =
+ isInputWrapped &&
+ !isShowingCompactMessage &&
+ apiKeyStatus !== 'invalid' &&
+ apiKeyStatus !== 'missing' &&
+ editor !== undefined
+
+ // Show external editor hint as notification when input is wrapped
+ useEffect(() => {
+ if (shouldShowExternalEditorHint && editor) {
+ logEvent('tengu_external_editor_hint_shown', {})
+ addNotification({
+ key: 'external-editor-hint',
+ jsx: (
+
+
+
+ ),
+ priority: 'immediate',
+ timeoutMs: 5000,
+ })
+ } else {
+ removeNotification('external-editor-hint')
+ }
+ }, [
+ shouldShowExternalEditorHint,
+ editor,
addNotification,
- removeNotification
- } = useNotifications();
- const claudeAiLimits = useClaudeAiLimits();
- let t5;
- let t6;
- if ($[5] !== addNotification) {
- t5 = () => {
- setEnvHookNotifier((text, isError) => {
- addNotification({
- key: "env-hook",
- text,
- color: isError ? "error" : undefined,
- priority: isError ? "medium" : "low",
- timeoutMs: isError ? 8000 : 5000
- });
- });
- return _temp2;
- };
- t6 = [addNotification];
- $[5] = addNotification;
- $[6] = t5;
- $[7] = t6;
- } else {
- t5 = $[6];
- t6 = $[7];
- }
- useEffect(t5, t6);
- const shouldShowIdeSelection = ideStatus === "connected" && (ideSelection?.filePath || ideSelection?.text && ideSelection.lineCount > 0);
- const shouldShowAutoUpdater = !shouldShowIdeSelection || isAutoUpdating || autoUpdaterResult?.status !== "success";
- const isInOverageMode = claudeAiLimits.isUsingOverage;
- let t7;
- if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
- t7 = getSubscriptionType();
- $[8] = t7;
- } else {
- t7 = $[8];
- }
- const subscriptionType = t7;
- const isTeamOrEnterprise = subscriptionType === "team" || subscriptionType === "enterprise";
- let t8;
- if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
- t8 = getExternalEditor();
- $[9] = t8;
- } else {
- t8 = $[9];
- }
- const editor = t8;
- const shouldShowExternalEditorHint = isInputWrapped && !isShowingCompactMessage && apiKeyStatus !== "invalid" && apiKeyStatus !== "missing" && editor !== undefined;
- let t10;
- let t9;
- if ($[10] !== addNotification || $[11] !== removeNotification || $[12] !== shouldShowExternalEditorHint) {
- t9 = () => {
- if (shouldShowExternalEditorHint && editor) {
- logEvent("tengu_external_editor_hint_shown", {});
- addNotification({
- key: "external-editor-hint",
- jsx: ,
- 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 = ;
- $[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 = {t13};
- $[31] = t11;
- $[32] = t13;
- $[33] = t14;
- } else {
- t14 = $[33];
- }
- return t14;
-}
-function _temp2() {
- return setEnvHookNotifier(null);
-}
-function _temp(s) {
- return s.notifications;
+ removeNotification,
+ ])
+
+ return (
+
+
+
+
+
+ )
}
+
function NotificationContent({
ideSelection,
mcpClients,
@@ -229,103 +212,155 @@ function NotificationContent({
isAutoUpdating,
isShowingCompactMessage,
onAutoUpdaterResult,
- onChangeIsUpdating
+ onChangeIsUpdating,
}: {
- ideSelection: IDESelection | undefined;
- mcpClients?: MCPServerConnection[];
+ ideSelection: IDESelection | undefined
+ mcpClients?: MCPServerConnection[]
notifications: {
- current: Notification | null;
- queue: Notification[];
- };
- isInOverageMode: boolean;
- isTeamOrEnterprise: boolean;
- apiKeyStatus: VerificationStatus;
- debug: boolean;
- verbose: boolean;
- tokenUsage: number;
- mainLoopModel: string;
- shouldShowAutoUpdater: boolean;
- autoUpdaterResult: AutoUpdaterResult | null;
- isAutoUpdating: boolean;
- isShowingCompactMessage: boolean;
- onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
- onChangeIsUpdating: (isUpdating: boolean) => void;
+ current: Notification | null
+ queue: Notification[]
+ }
+ isInOverageMode: boolean
+ isTeamOrEnterprise: boolean
+ apiKeyStatus: VerificationStatus
+ debug: boolean
+ verbose: boolean
+ tokenUsage: number
+ mainLoopModel: string
+ shouldShowAutoUpdater: boolean
+ autoUpdaterResult: AutoUpdaterResult | null
+ isAutoUpdating: boolean
+ isShowingCompactMessage: boolean
+ onAutoUpdaterResult: (result: AutoUpdaterResult) => void
+ onChangeIsUpdating: (isUpdating: boolean) => void
}): ReactNode {
// Poll apiKeyHelper inflight state to show slow-helper notice.
// Gated on configuration — most users never set apiKeyHelper, so the
// effect is a no-op for them (no interval allocated).
- const [apiKeyHelperSlow, setApiKeyHelperSlow] = useState(null);
+ const [apiKeyHelperSlow, setApiKeyHelperSlow] = useState(null)
useEffect(() => {
- if (!getConfiguredApiKeyHelper()) return;
- const interval = setInterval((setSlow: React.Dispatch>) => {
- const ms = getApiKeyHelperElapsedMs();
- const next = ms >= 10_000 ? formatDuration(ms) : null;
- setSlow(prev => next === prev ? prev : next);
- }, 1000, setApiKeyHelperSlow);
- return () => clearInterval(interval);
- }, []);
+ if (!getConfiguredApiKeyHelper()) return
+ const interval = setInterval(
+ (setSlow: React.Dispatch>) => {
+ const ms = getApiKeyHelperElapsedMs()
+ const next = ms >= 10_000 ? formatDuration(ms) : null
+ setSlow(prev => (next === prev ? prev : next))
+ },
+ 1000,
+ setApiKeyHelperSlow,
+ )
+ return () => clearInterval(interval)
+ }, [])
// Voice state (VOICE_MODE builds only, runtime-gated by GrowthBook)
- const voiceState = feature('VOICE_MODE') ?
+ const voiceState = feature('VOICE_MODE')
+ ? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+ useVoiceState(s => s.voiceState)
+ : ('idle' as const)
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- useVoiceState(s => s.voiceState) : 'idle' as const;
- // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false;
- const voiceError = feature('VOICE_MODE') ?
- // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- useVoiceState(s_0 => s_0.voiceError) : null;
- const isBriefOnly = feature('KAIROS') || feature('KAIROS_BRIEF') ?
- // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- useAppState(s_1 => s_1.isBriefOnly) : false;
+ const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false
+ const voiceError = feature('VOICE_MODE')
+ ? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+ useVoiceState(s => s.voiceError)
+ : null
+ const isBriefOnly =
+ feature('KAIROS') || feature('KAIROS_BRIEF')
+ ? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+ useAppState(s => s.isBriefOnly)
+ : false
// When voice is actively recording or processing, replace all
// notifications with just the voice indicator.
- if (feature('VOICE_MODE') && voiceEnabled && (voiceState === 'recording' || voiceState === 'processing')) {
- return ;
+ if (
+ feature('VOICE_MODE') &&
+ voiceEnabled &&
+ (voiceState === 'recording' || voiceState === 'processing')
+ ) {
+ return
}
- return <>
+
+ return (
+ <>
- {notifications.current && ('jsx' in notifications.current ?
+ {notifications.current &&
+ ('jsx' in notifications.current ? (
+
{notifications.current.jsx}
- :
+
+ ) : (
+
{notifications.current.text}
- )}
- {isInOverageMode && !isTeamOrEnterprise &&
+
+ ))}
+ {isInOverageMode && !isTeamOrEnterprise && (
+
Now using extra usage
- }
- {apiKeyHelperSlow &&
+
+ )}
+ {apiKeyHelperSlow && (
+
apiKeyHelper is taking a while{' '}
({apiKeyHelperSlow})
- }
- {(apiKeyStatus === 'invalid' || apiKeyStatus === 'missing') &&
+
+ )}
+ {(apiKeyStatus === 'invalid' || apiKeyStatus === 'missing') && (
+
- {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'}
- }
- {debug &&
+
+ )}
+ {debug && (
+
Debug mode
- }
- {apiKeyStatus !== 'invalid' && apiKeyStatus !== 'missing' && verbose &&
+
+ )}
+ {apiKeyStatus !== 'invalid' && apiKeyStatus !== 'missing' && verbose && (
+
{tokenUsage} tokens
- }
- {!isBriefOnly && }
- {shouldShowAutoUpdater && }
- {feature('VOICE_MODE') ? voiceEnabled && voiceError &&
+
+ )}
+ {!isBriefOnly && (
+
+ )}
+ {shouldShowAutoUpdater && (
+
+ )}
+ {feature('VOICE_MODE')
+ ? voiceEnabled &&
+ voiceError && (
+
{voiceError}
- : null}
+
+ )
+ : null}
- >;
+ >
+ )
}
diff --git a/src/components/PromptInput/PromptInput.tsx b/src/components/PromptInput/PromptInput.tsx
index bc80851fb..a678d41f6 100644
--- a/src/components/PromptInput/PromptInput.tsx
+++ b/src/components/PromptInput/PromptInput.tsx
@@ -1,196 +1,317 @@
-import { feature } from 'bun:bundle';
-import chalk from 'chalk';
-import * as path from 'path';
-import * as React from 'react';
-import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
-import { useNotifications } from 'src/context/notifications.js';
-import { useCommandQueue } from 'src/hooks/useCommandQueue.js';
-import { type IDEAtMentioned, useIdeAtMentioned } from 'src/hooks/useIdeAtMentioned.js';
-import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
-import { type AppState, useAppState, useAppStateStore, useSetAppState } from 'src/state/AppState.js';
-import type { FooterItem } from 'src/state/AppStateStore.js';
-import { getCwd } from 'src/utils/cwd.js';
-import { isQueuedCommandEditable, popAllEditable } from 'src/utils/messageQueueManager.js';
-import stripAnsi from 'strip-ansi';
-import { companionReservedColumns } from '../../buddy/CompanionSprite.js';
-import { findBuddyTriggerPositions, useBuddyNotification } from '../../buddy/useBuddyNotification.js';
-import { FastModePicker } from '../../commands/fast/fast.js';
-import { isUltrareviewEnabled } from '../../commands/review/ultrareviewEnabled.js';
-import { getNativeCSIuTerminalDisplayName } from '../../commands/terminalSetup/terminalSetup.js';
-import { type Command, hasCommand } from '../../commands.js';
-import { useIsModalOverlayActive } from '../../context/overlayContext.js';
-import { useSetPromptOverlayDialog } from '../../context/promptOverlayContext.js';
-import { formatImageRef, formatPastedTextRef, getPastedTextRefNumLines, parseReferences } from '../../history.js';
-import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js';
-import { type HistoryMode, useArrowKeyHistory } from '../../hooks/useArrowKeyHistory.js';
-import { useDoublePress } from '../../hooks/useDoublePress.js';
-import { useHistorySearch } from '../../hooks/useHistorySearch.js';
-import type { IDESelection } from '../../hooks/useIdeSelection.js';
-import { useInputBuffer } from '../../hooks/useInputBuffer.js';
-import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
-import { usePromptSuggestion } from '../../hooks/usePromptSuggestion.js';
-import { useTerminalSize } from '../../hooks/useTerminalSize.js';
-import { useTypeahead } from '../../hooks/useTypeahead.js';
-import type { BorderTextOptions } from '../../ink/render-border.js';
-import { stringWidth } from '../../ink/stringWidth.js';
-import { Box, type ClickEvent, type Key, Text, useInput } from '../../ink.js';
-import { useOptionalKeybindingContext } from '../../keybindings/KeybindingContext.js';
-import { getShortcutDisplay } from '../../keybindings/shortcutFormat.js';
-import { useKeybinding, useKeybindings } from '../../keybindings/useKeybinding.js';
-import type { MCPServerConnection } from '../../services/mcp/types.js';
-import { abortPromptSuggestion, logSuggestionSuppressed } from '../../services/PromptSuggestion/promptSuggestion.js';
-import { type ActiveSpeculationState, abortSpeculation } from '../../services/PromptSuggestion/speculation.js';
-import { getActiveAgentForInput, getViewedTeammateTask } from '../../state/selectors.js';
-import { enterTeammateView, exitTeammateView, stopOrDismissAgent } from '../../state/teammateViewHelpers.js';
-import type { ToolPermissionContext } from '../../Tool.js';
-import { getRunningTeammatesSorted } from '../../tasks/InProcessTeammateTask/InProcessTeammateTask.js';
-import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js';
-import { isPanelAgentTask, type LocalAgentTaskState } from '../../tasks/LocalAgentTask/LocalAgentTask.js';
-import { isBackgroundTask } from '../../tasks/types.js';
-import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from '../../tools/AgentTool/agentColorManager.js';
-import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js';
-import type { Message } from '../../types/message.js';
-import type { PermissionMode } from '../../types/permissions.js';
-import type { BaseTextInputProps, PromptInputMode, VimMode } from '../../types/textInputTypes.js';
-import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js';
-import { count } from '../../utils/array.js';
-import type { AutoUpdaterResult } from '../../utils/autoUpdater.js';
-import { Cursor } from '../../utils/Cursor.js';
-import { getGlobalConfig, type PastedContent, saveGlobalConfig } from '../../utils/config.js';
-import { logForDebugging } from '../../utils/debug.js';
-import { parseDirectMemberMessage, sendDirectMemberMessage } from '../../utils/directMemberMessage.js';
-import type { EffortLevel } from '../../utils/effort.js';
-import { env } from '../../utils/env.js';
-import { errorMessage } from '../../utils/errors.js';
-import { isBilledAsExtraUsage } from '../../utils/extraUsage.js';
-import { getFastModeUnavailableReason, isFastModeAvailable, isFastModeCooldown, isFastModeEnabled, isFastModeSupportedByModel } from '../../utils/fastMode.js';
-import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js';
-import type { PromptInputHelpers } from '../../utils/handlePromptSubmit.js';
-import { getImageFromClipboard, PASTE_THRESHOLD } from '../../utils/imagePaste.js';
-import type { ImageDimensions } from '../../utils/imageResizer.js';
-import { cacheImagePath, storeImage } from '../../utils/imageStore.js';
-import { isMacosOptionChar, MACOS_OPTION_SPECIAL_CHARS } from '../../utils/keyboardShortcuts.js';
-import { logError } from '../../utils/log.js';
-import { isOpus1mMergeEnabled, modelDisplayString } from '../../utils/model/model.js';
-import { setAutoModeActive } from '../../utils/permissions/autoModeState.js';
-import { cyclePermissionMode, getNextPermissionMode } from '../../utils/permissions/getNextPermissionMode.js';
-import { transitionPermissionMode } from '../../utils/permissions/permissionSetup.js';
-import { getPlatform } from '../../utils/platform.js';
-import type { ProcessUserInputContext } from '../../utils/processUserInput/processUserInput.js';
-import { editPromptInEditor } from '../../utils/promptEditor.js';
-import { hasAutoModeOptIn } from '../../utils/settings/settings.js';
-import { findBtwTriggerPositions } from '../../utils/sideQuestion.js';
-import { findSlashCommandPositions } from '../../utils/suggestions/commandSuggestions.js';
-import { findSlackChannelPositions, getKnownChannelsVersion, hasSlackMcpServer, subscribeKnownChannels } from '../../utils/suggestions/slackChannelSuggestions.js';
-import { isInProcessEnabled } from '../../utils/swarm/backends/registry.js';
-import { syncTeammateMode } from '../../utils/swarm/teamHelpers.js';
-import type { TeamSummary } from '../../utils/teamDiscovery.js';
-import { getTeammateColor } from '../../utils/teammate.js';
-import { isInProcessTeammate } from '../../utils/teammateContext.js';
-import { writeToMailbox } from '../../utils/teammateMailbox.js';
-import type { TextHighlight } from '../../utils/textHighlighting.js';
-import type { Theme } from '../../utils/theme.js';
-import { findThinkingTriggerPositions, getRainbowColor, isUltrathinkEnabled } from '../../utils/thinking.js';
-import { findTokenBudgetPositions } from '../../utils/tokenBudget.js';
-import { findUltraplanTriggerPositions, findUltrareviewTriggerPositions } from '../../utils/ultraplan/keyword.js';
-import { AutoModeOptInDialog } from '../AutoModeOptInDialog.js';
-import { BridgeDialog } from '../BridgeDialog.js';
-import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
-import { getVisibleAgentTasks, useCoordinatorTaskCount } from '../CoordinatorAgentStatus.js';
-import { getEffortNotificationText } from '../EffortIndicator.js';
-import { getFastIconString } from '../FastIcon.js';
-import { GlobalSearchDialog } from '../GlobalSearchDialog.js';
-import { HistorySearchDialog } from '../HistorySearchDialog.js';
-import { ModelPicker } from '../ModelPicker.js';
-import { QuickOpenDialog } from '../QuickOpenDialog.js';
-import TextInput from '../TextInput.js';
-import { ThinkingToggle } from '../ThinkingToggle.js';
-import { BackgroundTasksDialog } from '../tasks/BackgroundTasksDialog.js';
-import { shouldHideTasksFooter } from '../tasks/taskStatusUtils.js';
-import { TeamsDialog } from '../teams/TeamsDialog.js';
-import VimTextInput from '../VimTextInput.js';
-import { getModeFromInput, getValueFromInput } from './inputModes.js';
-import { FOOTER_TEMPORARY_STATUS_TIMEOUT, Notifications } from './Notifications.js';
-import PromptInputFooter from './PromptInputFooter.js';
-import type { SuggestionItem } from './PromptInputFooterSuggestions.js';
-import { PromptInputModeIndicator } from './PromptInputModeIndicator.js';
-import { PromptInputQueuedCommands } from './PromptInputQueuedCommands.js';
-import { PromptInputStashNotice } from './PromptInputStashNotice.js';
-import { useMaybeTruncateInput } from './useMaybeTruncateInput.js';
-import { usePromptInputPlaceholder } from './usePromptInputPlaceholder.js';
-import { useShowFastIconHint } from './useShowFastIconHint.js';
-import { useSwarmBanner } from './useSwarmBanner.js';
-import { isNonSpacePrintable, isVimModeEnabled } from './utils.js';
+import { feature } from 'bun:bundle'
+import chalk from 'chalk'
+import * as path from 'path'
+import * as React from 'react'
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ useSyncExternalStore,
+} from 'react'
+import { useNotifications } from 'src/context/notifications.js'
+import { useCommandQueue } from 'src/hooks/useCommandQueue.js'
+import {
+ type IDEAtMentioned,
+ useIdeAtMentioned,
+} from 'src/hooks/useIdeAtMentioned.js'
+import {
+ type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ logEvent,
+} from 'src/services/analytics/index.js'
+import {
+ type AppState,
+ useAppState,
+ useAppStateStore,
+ useSetAppState,
+} from 'src/state/AppState.js'
+import type { FooterItem } from 'src/state/AppStateStore.js'
+import { getCwd } from 'src/utils/cwd.js'
+import {
+ isQueuedCommandEditable,
+ popAllEditable,
+} from 'src/utils/messageQueueManager.js'
+import stripAnsi from 'strip-ansi'
+import { companionReservedColumns } from '../../buddy/CompanionSprite.js'
+import {
+ findBuddyTriggerPositions,
+ useBuddyNotification,
+} from '../../buddy/useBuddyNotification.js'
+import { FastModePicker } from '../../commands/fast/fast.js'
+import { isUltrareviewEnabled } from '../../commands/review/ultrareviewEnabled.js'
+import { getNativeCSIuTerminalDisplayName } from '../../commands/terminalSetup/terminalSetup.js'
+import { type Command, hasCommand } from '../../commands.js'
+import { useIsModalOverlayActive } from '../../context/overlayContext.js'
+import { useSetPromptOverlayDialog } from '../../context/promptOverlayContext.js'
+import {
+ formatImageRef,
+ formatPastedTextRef,
+ getPastedTextRefNumLines,
+ parseReferences,
+} from '../../history.js'
+import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
+import {
+ type HistoryMode,
+ useArrowKeyHistory,
+} from '../../hooks/useArrowKeyHistory.js'
+import { useDoublePress } from '../../hooks/useDoublePress.js'
+import { useHistorySearch } from '../../hooks/useHistorySearch.js'
+import type { IDESelection } from '../../hooks/useIdeSelection.js'
+import { useInputBuffer } from '../../hooks/useInputBuffer.js'
+import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
+import { usePromptSuggestion } from '../../hooks/usePromptSuggestion.js'
+import { useTerminalSize } from '../../hooks/useTerminalSize.js'
+import { useTypeahead } from '../../hooks/useTypeahead.js'
+import type { BorderTextOptions } from '../../ink/render-border.js'
+import { stringWidth } from '../../ink/stringWidth.js'
+import { Box, type ClickEvent, type Key, Text, useInput } from '../../ink.js'
+import { useOptionalKeybindingContext } from '../../keybindings/KeybindingContext.js'
+import { getShortcutDisplay } from '../../keybindings/shortcutFormat.js'
+import {
+ useKeybinding,
+ useKeybindings,
+} from '../../keybindings/useKeybinding.js'
+import type { MCPServerConnection } from '../../services/mcp/types.js'
+import {
+ abortPromptSuggestion,
+ logSuggestionSuppressed,
+} from '../../services/PromptSuggestion/promptSuggestion.js'
+import {
+ type ActiveSpeculationState,
+ abortSpeculation,
+} from '../../services/PromptSuggestion/speculation.js'
+import {
+ getActiveAgentForInput,
+ getViewedTeammateTask,
+} from '../../state/selectors.js'
+import {
+ enterTeammateView,
+ exitTeammateView,
+ stopOrDismissAgent,
+} from '../../state/teammateViewHelpers.js'
+import type { ToolPermissionContext } from '../../Tool.js'
+import { getRunningTeammatesSorted } from '../../tasks/InProcessTeammateTask/InProcessTeammateTask.js'
+import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js'
+import {
+ isPanelAgentTask,
+ type LocalAgentTaskState,
+} from '../../tasks/LocalAgentTask/LocalAgentTask.js'
+import { isBackgroundTask } from '../../tasks/types.js'
+import {
+ AGENT_COLOR_TO_THEME_COLOR,
+ AGENT_COLORS,
+ type AgentColorName,
+} from '../../tools/AgentTool/agentColorManager.js'
+import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js'
+import type { Message } from '../../types/message.js'
+import type { PermissionMode } from '../../types/permissions.js'
+import type {
+ BaseTextInputProps,
+ PromptInputMode,
+ VimMode,
+} from '../../types/textInputTypes.js'
+import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
+import { count } from '../../utils/array.js'
+import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
+import { Cursor } from '../../utils/Cursor.js'
+import {
+ getGlobalConfig,
+ type PastedContent,
+ saveGlobalConfig,
+} from '../../utils/config.js'
+import { logForDebugging } from '../../utils/debug.js'
+import {
+ parseDirectMemberMessage,
+ sendDirectMemberMessage,
+} from '../../utils/directMemberMessage.js'
+import type { EffortLevel } from '../../utils/effort.js'
+import { env } from '../../utils/env.js'
+import { errorMessage } from '../../utils/errors.js'
+import { isBilledAsExtraUsage } from '../../utils/extraUsage.js'
+import {
+ getFastModeUnavailableReason,
+ isFastModeAvailable,
+ isFastModeCooldown,
+ isFastModeEnabled,
+ isFastModeSupportedByModel,
+} from '../../utils/fastMode.js'
+import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
+import type { PromptInputHelpers } from '../../utils/handlePromptSubmit.js'
+import {
+ getImageFromClipboard,
+ PASTE_THRESHOLD,
+} from '../../utils/imagePaste.js'
+import type { ImageDimensions } from '../../utils/imageResizer.js'
+import { cacheImagePath, storeImage } from '../../utils/imageStore.js'
+import {
+ isMacosOptionChar,
+ MACOS_OPTION_SPECIAL_CHARS,
+} from '../../utils/keyboardShortcuts.js'
+import { logError } from '../../utils/log.js'
+import {
+ isOpus1mMergeEnabled,
+ modelDisplayString,
+} from '../../utils/model/model.js'
+import { setAutoModeActive } from '../../utils/permissions/autoModeState.js'
+import {
+ cyclePermissionMode,
+ getNextPermissionMode,
+} from '../../utils/permissions/getNextPermissionMode.js'
+import { transitionPermissionMode } from '../../utils/permissions/permissionSetup.js'
+import { getPlatform } from '../../utils/platform.js'
+import type { ProcessUserInputContext } from '../../utils/processUserInput/processUserInput.js'
+import { editPromptInEditor } from '../../utils/promptEditor.js'
+import { hasAutoModeOptIn } from '../../utils/settings/settings.js'
+import { findBtwTriggerPositions } from '../../utils/sideQuestion.js'
+import { findSlashCommandPositions } from '../../utils/suggestions/commandSuggestions.js'
+import {
+ findSlackChannelPositions,
+ getKnownChannelsVersion,
+ hasSlackMcpServer,
+ subscribeKnownChannels,
+} from '../../utils/suggestions/slackChannelSuggestions.js'
+import { isInProcessEnabled } from '../../utils/swarm/backends/registry.js'
+import { syncTeammateMode } from '../../utils/swarm/teamHelpers.js'
+import type { TeamSummary } from '../../utils/teamDiscovery.js'
+import { getTeammateColor } from '../../utils/teammate.js'
+import { isInProcessTeammate } from '../../utils/teammateContext.js'
+import { writeToMailbox } from '../../utils/teammateMailbox.js'
+import type { TextHighlight } from '../../utils/textHighlighting.js'
+import type { Theme } from '../../utils/theme.js'
+import {
+ findThinkingTriggerPositions,
+ getRainbowColor,
+ isUltrathinkEnabled,
+} from '../../utils/thinking.js'
+import { findTokenBudgetPositions } from '../../utils/tokenBudget.js'
+import {
+ findUltraplanTriggerPositions,
+ findUltrareviewTriggerPositions,
+} from '../../utils/ultraplan/keyword.js'
+import { AutoModeOptInDialog } from '../AutoModeOptInDialog.js'
+import { BridgeDialog } from '../BridgeDialog.js'
+import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
+import {
+ getVisibleAgentTasks,
+ useCoordinatorTaskCount,
+} from '../CoordinatorAgentStatus.js'
+import { getEffortNotificationText } from '../EffortIndicator.js'
+import { getFastIconString } from '../FastIcon.js'
+import { GlobalSearchDialog } from '../GlobalSearchDialog.js'
+import { HistorySearchDialog } from '../HistorySearchDialog.js'
+import { ModelPicker } from '../ModelPicker.js'
+import { QuickOpenDialog } from '../QuickOpenDialog.js'
+import TextInput from '../TextInput.js'
+import { ThinkingToggle } from '../ThinkingToggle.js'
+import { BackgroundTasksDialog } from '../tasks/BackgroundTasksDialog.js'
+import { shouldHideTasksFooter } from '../tasks/taskStatusUtils.js'
+import { TeamsDialog } from '../teams/TeamsDialog.js'
+import VimTextInput from '../VimTextInput.js'
+import { getModeFromInput, getValueFromInput } from './inputModes.js'
+import {
+ FOOTER_TEMPORARY_STATUS_TIMEOUT,
+ Notifications,
+} from './Notifications.js'
+import PromptInputFooter from './PromptInputFooter.js'
+import type { SuggestionItem } from './PromptInputFooterSuggestions.js'
+import { PromptInputModeIndicator } from './PromptInputModeIndicator.js'
+import { PromptInputQueuedCommands } from './PromptInputQueuedCommands.js'
+import { PromptInputStashNotice } from './PromptInputStashNotice.js'
+import { useMaybeTruncateInput } from './useMaybeTruncateInput.js'
+import { usePromptInputPlaceholder } from './usePromptInputPlaceholder.js'
+import { useShowFastIconHint } from './useShowFastIconHint.js'
+import { useSwarmBanner } from './useSwarmBanner.js'
+import { isNonSpacePrintable, isVimModeEnabled } from './utils.js'
+
type Props = {
- debug: boolean;
- ideSelection: IDESelection | undefined;
- toolPermissionContext: ToolPermissionContext;
- setToolPermissionContext: (ctx: ToolPermissionContext) => void;
- apiKeyStatus: VerificationStatus;
- commands: Command[];
- agents: AgentDefinition[];
- isLoading: boolean;
- verbose: boolean;
- messages: Message[];
- onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
- autoUpdaterResult: AutoUpdaterResult | null;
- input: string;
- onInputChange: (value: string) => void;
- mode: PromptInputMode;
- onModeChange: (mode: PromptInputMode) => void;
- stashedPrompt: {
- text: string;
- cursorOffset: number;
- pastedContents: Record;
- } | undefined;
- setStashedPrompt: (value: {
- text: string;
- cursorOffset: number;
- pastedContents: Record;
- } | undefined) => void;
- submitCount: number;
- onShowMessageSelector: () => void;
+ debug: boolean
+ ideSelection: IDESelection | undefined
+ toolPermissionContext: ToolPermissionContext
+ setToolPermissionContext: (ctx: ToolPermissionContext) => void
+ apiKeyStatus: VerificationStatus
+ commands: Command[]
+ agents: AgentDefinition[]
+ isLoading: boolean
+ verbose: boolean
+ messages: Message[]
+ onAutoUpdaterResult: (result: AutoUpdaterResult) => void
+ autoUpdaterResult: AutoUpdaterResult | null
+ input: string
+ onInputChange: (value: string) => void
+ mode: PromptInputMode
+ onModeChange: (mode: PromptInputMode) => void
+ stashedPrompt:
+ | {
+ text: string
+ cursorOffset: number
+ pastedContents: Record
+ }
+ | undefined
+ setStashedPrompt: (
+ value:
+ | {
+ text: string
+ cursorOffset: number
+ pastedContents: Record
+ }
+ | undefined,
+ ) => void
+ submitCount: number
+ onShowMessageSelector: () => void
/** Fullscreen message actions: shift+↑ enters cursor. */
- onMessageActionsEnter?: () => void;
- mcpClients: MCPServerConnection[];
- pastedContents: Record;
- setPastedContents: React.Dispatch>>;
- vimMode: VimMode;
- setVimMode: (mode: VimMode) => void;
- showBashesDialog: string | boolean;
- setShowBashesDialog: (show: string | boolean) => void;
- onExit: () => void;
- getToolUseContext: (messages: Message[], newMessages: Message[], abortController: AbortController, mainLoopModel: string) => ProcessUserInputContext;
- onSubmit: (input: string, helpers: PromptInputHelpers, speculationAccept?: {
- state: ActiveSpeculationState;
- speculationSessionTimeSavedMs: number;
- setAppState: (f: (prev: AppState) => AppState) => void;
- }, options?: {
- fromKeybinding?: boolean;
- }) => Promise;
- onAgentSubmit?: (input: string, task: InProcessTeammateTaskState | LocalAgentTaskState, helpers: PromptInputHelpers) => Promise;
- isSearchingHistory: boolean;
- setIsSearchingHistory: (isSearching: boolean) => void;
- onDismissSideQuestion?: () => void;
- isSideQuestionVisible?: boolean;
- helpOpen: boolean;
- setHelpOpen: React.Dispatch>;
- hasSuppressedDialogs?: boolean;
- isLocalJSXCommandActive?: boolean;
+ onMessageActionsEnter?: () => void
+ mcpClients: MCPServerConnection[]
+ pastedContents: Record
+ setPastedContents: React.Dispatch<
+ React.SetStateAction>
+ >
+ vimMode: VimMode
+ setVimMode: (mode: VimMode) => void
+ showBashesDialog: string | boolean
+ setShowBashesDialog: (show: string | boolean) => void
+ onExit: () => void
+ getToolUseContext: (
+ messages: Message[],
+ newMessages: Message[],
+ abortController: AbortController,
+ mainLoopModel: string,
+ ) => ProcessUserInputContext
+ onSubmit: (
+ input: string,
+ helpers: PromptInputHelpers,
+ speculationAccept?: {
+ state: ActiveSpeculationState
+ speculationSessionTimeSavedMs: number
+ setAppState: (f: (prev: AppState) => AppState) => void
+ },
+ options?: { fromKeybinding?: boolean },
+ ) => Promise
+ onAgentSubmit?: (
+ input: string,
+ task: InProcessTeammateTaskState | LocalAgentTaskState,
+ helpers: PromptInputHelpers,
+ ) => Promise
+ isSearchingHistory: boolean
+ setIsSearchingHistory: (isSearching: boolean) => void
+ onDismissSideQuestion?: () => void
+ isSideQuestionVisible?: boolean
+ helpOpen: boolean
+ setHelpOpen: React.Dispatch>
+ hasSuppressedDialogs?: boolean
+ isLocalJSXCommandActive?: boolean
insertTextRef?: React.MutableRefObject<{
- insert: (text: string) => void;
- setInputWithCursor: (value: string, cursor: number) => void;
- cursorOffset: number;
- } | null>;
- voiceInterimRange?: {
- start: number;
- end: number;
- } | null;
-};
+ insert: (text: string) => void
+ setInputWithCursor: (value: string, cursor: number) => void
+ cursorOffset: number
+ } | null>
+ voiceInterimRange?: { start: number; end: number } | null
+}
// Bottom slot has maxHeight="50%"; reserve lines for footer, border, status.
-const PROMPT_FOOTER_LINES = 5;
-const MIN_INPUT_VIEWPORT_LINES = 3;
+const PROMPT_FOOTER_LINES = 5
+const MIN_INPUT_VIEWPORT_LINES = 3
+
function PromptInput({
debug,
ideSelection,
@@ -233,276 +354,359 @@ function PromptInput({
hasSuppressedDialogs,
isLocalJSXCommandActive = false,
insertTextRef,
- voiceInterimRange
+ voiceInterimRange,
}: Props): React.ReactNode {
- const mainLoopModel = useMainLoopModel();
+ const mainLoopModel = useMainLoopModel()
// A local-jsx command (e.g., /mcp while agent is running) renders a full-
// screen dialog on top of PromptInput via the immediate-command path with
// shouldHidePromptInput: false. Those dialogs don't register in the overlay
// system, so treat them as a modal overlay here to stop navigation keys from
// leaking into TextInput/footer handlers and stacking a second dialog.
- const isModalOverlayActive = useIsModalOverlayActive() || isLocalJSXCommandActive;
- const [isAutoUpdating, setIsAutoUpdating] = useState(false);
+ const isModalOverlayActive =
+ useIsModalOverlayActive() || isLocalJSXCommandActive
+ const [isAutoUpdating, setIsAutoUpdating] = useState(false)
const [exitMessage, setExitMessage] = useState<{
- show: boolean;
- key?: string;
- }>({
- show: false
- });
- const [cursorOffset, setCursorOffset] = useState(input.length);
+ show: boolean
+ key?: string
+ }>({ show: false })
+ const [cursorOffset, setCursorOffset] = useState(input.length)
// Track the last input value set via internal handlers so we can detect
// external input changes (e.g. speech-to-text injection) and move cursor to end.
- const lastInternalInputRef = React.useRef(input);
+ const lastInternalInputRef = React.useRef(input)
if (input !== lastInternalInputRef.current) {
// Input changed externally (not through any internal handler) — move cursor to end
- setCursorOffset(input.length);
- lastInternalInputRef.current = input;
+ setCursorOffset(input.length)
+ lastInternalInputRef.current = input
}
// Wrap onInputChange to track internal changes before they trigger re-render
- const trackAndSetInput = React.useCallback((value: string) => {
- lastInternalInputRef.current = value;
- onInputChange(value);
- }, [onInputChange]);
+ const trackAndSetInput = React.useCallback(
+ (value: string) => {
+ lastInternalInputRef.current = value
+ onInputChange(value)
+ },
+ [onInputChange],
+ )
// Expose an insertText function so callers (e.g. STT) can splice text at the
// current cursor position instead of replacing the entire input.
if (insertTextRef) {
insertTextRef.current = {
cursorOffset,
insert: (text: string) => {
- const needsSpace = cursorOffset === input.length && input.length > 0 && !/\s$/.test(input);
- const insertText = needsSpace ? ' ' + text : text;
- const newValue = input.slice(0, cursorOffset) + insertText + input.slice(cursorOffset);
- lastInternalInputRef.current = newValue;
- onInputChange(newValue);
- setCursorOffset(cursorOffset + insertText.length);
+ const needsSpace =
+ cursorOffset === input.length &&
+ input.length > 0 &&
+ !/\s$/.test(input)
+ const insertText = needsSpace ? ' ' + text : text
+ const newValue =
+ input.slice(0, cursorOffset) + insertText + input.slice(cursorOffset)
+ lastInternalInputRef.current = newValue
+ onInputChange(newValue)
+ setCursorOffset(cursorOffset + insertText.length)
},
setInputWithCursor: (value: string, cursor: number) => {
- lastInternalInputRef.current = value;
- onInputChange(value);
- setCursorOffset(cursor);
- }
- };
+ lastInternalInputRef.current = value
+ onInputChange(value)
+ setCursorOffset(cursor)
+ },
+ }
}
- const store = useAppStateStore();
- const setAppState = useSetAppState();
- const tasks = useAppState(s => s.tasks);
- const replBridgeConnected = useAppState(s => s.replBridgeConnected);
- const replBridgeExplicit = useAppState(s => s.replBridgeExplicit);
- const replBridgeReconnecting = useAppState(s => s.replBridgeReconnecting);
+ const store = useAppStateStore()
+ const setAppState = useSetAppState()
+ const tasks = useAppState(s => s.tasks)
+ const replBridgeConnected = useAppState(s => s.replBridgeConnected)
+ const replBridgeExplicit = useAppState(s => s.replBridgeExplicit)
+ const replBridgeReconnecting = useAppState(s => s.replBridgeReconnecting)
// Must match BridgeStatusIndicator's render condition (PromptInputFooter.tsx) —
// the pill returns null for implicit-and-not-reconnecting, so nav must too,
// otherwise bridge becomes an invisible selection stop.
- const bridgeFooterVisible = replBridgeConnected && (replBridgeExplicit || replBridgeReconnecting);
+ const bridgeFooterVisible =
+ replBridgeConnected && (replBridgeExplicit || replBridgeReconnecting)
// Tmux pill (ant-only) — visible when there's an active tungsten session
- const hasTungstenSession = useAppState(s => (process.env.USER_TYPE) === 'ant' && s.tungstenActiveSession !== undefined);
- const tmuxFooterVisible = (process.env.USER_TYPE) === 'ant' && hasTungstenSession;
+ const hasTungstenSession = useAppState(
+ s =>
+ process.env.USER_TYPE === 'ant' && s.tungstenActiveSession !== undefined,
+ )
+ const tmuxFooterVisible =
+ process.env.USER_TYPE === 'ant' && hasTungstenSession
// WebBrowser pill — visible when a browser is open
- const bagelFooterVisible = useAppState(s => false);
- const teamContext = useAppState(s => s.teamContext);
- const queuedCommands = useCommandQueue();
- const promptSuggestionState = useAppState(s => s.promptSuggestion);
- const speculation = useAppState(s => s.speculation);
- const speculationSessionTimeSavedMs = useAppState(s => s.speculationSessionTimeSavedMs);
- const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId);
- const viewSelectionMode = useAppState(s => s.viewSelectionMode);
- const showSpinnerTree = useAppState(s => s.expandedView) === 'teammates';
- const {
- companion: _companion,
- companionMuted
- } = feature('BUDDY') ? getGlobalConfig() : {
- companion: undefined,
- companionMuted: undefined
- };
- const companionFooterVisible = !!_companion && !companionMuted;
+ const bagelFooterVisible = useAppState(s =>
+ false,
+ )
+ const teamContext = useAppState(s => s.teamContext)
+ const queuedCommands = useCommandQueue()
+ const promptSuggestionState = useAppState(s => s.promptSuggestion)
+ const speculation = useAppState(s => s.speculation)
+ const speculationSessionTimeSavedMs = useAppState(
+ s => s.speculationSessionTimeSavedMs,
+ )
+ const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId)
+ const viewSelectionMode = useAppState(s => s.viewSelectionMode)
+ const showSpinnerTree = useAppState(s => s.expandedView) === 'teammates'
+ const { companion: _companion, companionMuted } = feature('BUDDY')
+ ? getGlobalConfig()
+ : { companion: undefined, companionMuted: undefined }
+ const companionFooterVisible = !!_companion && !companionMuted
// Brief mode: BriefSpinner/BriefIdleStatus own the 2-row footprint above
// the input. Dropping marginTop here lets the spinner sit flush against
// the input bar. viewingAgentTaskId mirrors the gate on both (Spinner.tsx,
// REPL.tsx) — teammate view falls back to SpinnerWithVerbInner which has
// its own marginTop, so the gap stays even without ours.
- const briefOwnsGap = feature('KAIROS') || feature('KAIROS_BRIEF') ?
- // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- useAppState(s => s.isBriefOnly) && !viewingAgentTaskId : false;
- const mainLoopModel_ = useAppState(s => s.mainLoopModel);
- const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession);
- const thinkingEnabled = useAppState(s => s.thinkingEnabled);
- const isFastMode = useAppState(s => isFastModeEnabled() ? s.fastMode : false);
- const effortValue = useAppState(s => s.effortValue);
- const viewedTeammate = getViewedTeammateTask(store.getState());
- const viewingAgentName = viewedTeammate?.identity.agentName;
+ const briefOwnsGap =
+ feature('KAIROS') || feature('KAIROS_BRIEF')
+ ? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+ useAppState(s => s.isBriefOnly) && !viewingAgentTaskId
+ : false
+ const mainLoopModel_ = useAppState(s => s.mainLoopModel)
+ const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession)
+ const thinkingEnabled = useAppState(s => s.thinkingEnabled)
+ const isFastMode = useAppState(s =>
+ isFastModeEnabled() ? s.fastMode : false,
+ )
+ const effortValue = useAppState(s => s.effortValue)
+ const viewedTeammate = getViewedTeammateTask(store.getState())
+ const viewingAgentName = viewedTeammate?.identity.agentName
// identity.color is typed as `string | undefined` (not AgentColorName) because
// teammate identity comes from file-based config. Validate before casting to
// ensure we only use valid color names (falls back to cyan if invalid).
- const viewingAgentColor = viewedTeammate?.identity.color && AGENT_COLORS.includes(viewedTeammate.identity.color as AgentColorName) ? viewedTeammate.identity.color as AgentColorName : undefined;
+ const viewingAgentColor =
+ viewedTeammate?.identity.color &&
+ AGENT_COLORS.includes(viewedTeammate.identity.color as AgentColorName)
+ ? (viewedTeammate.identity.color as AgentColorName)
+ : undefined
// In-process teammates sorted alphabetically for footer team selector
- const inProcessTeammates = useMemo(() => getRunningTeammatesSorted(tasks), [tasks]);
+ const inProcessTeammates = useMemo(
+ () => getRunningTeammatesSorted(tasks),
+ [tasks],
+ )
// Team mode: all background tasks are in-process teammates
- const isTeammateMode = inProcessTeammates.length > 0 || viewedTeammate !== undefined;
+ const isTeammateMode =
+ inProcessTeammates.length > 0 || viewedTeammate !== undefined
// When viewing a teammate, show their permission mode in the footer instead of the leader's
const effectiveToolPermissionContext = useMemo((): ToolPermissionContext => {
if (viewedTeammate) {
return {
...toolPermissionContext,
- mode: viewedTeammate.permissionMode
- };
+ mode: viewedTeammate.permissionMode,
+ }
}
- return toolPermissionContext;
- }, [viewedTeammate, toolPermissionContext]);
- const {
- historyQuery,
- setHistoryQuery,
- historyMatch,
- historyFailedMatch
- } = useHistorySearch(entry => {
- setPastedContents(entry.pastedContents);
- void onSubmit(entry.display);
- }, input, trackAndSetInput, setCursorOffset, cursorOffset, onModeChange, mode, isSearchingHistory, setIsSearchingHistory, setPastedContents, pastedContents);
+ return toolPermissionContext
+ }, [viewedTeammate, toolPermissionContext])
+ const { historyQuery, setHistoryQuery, historyMatch, historyFailedMatch } =
+ useHistorySearch(
+ entry => {
+ setPastedContents(entry.pastedContents)
+ void onSubmit(entry.display)
+ },
+ input,
+ trackAndSetInput,
+ setCursorOffset,
+ cursorOffset,
+ onModeChange,
+ mode,
+ isSearchingHistory,
+ setIsSearchingHistory,
+ setPastedContents,
+ pastedContents,
+ )
// Counter for paste IDs (shared between images and text).
// Compute initial value once from existing messages (for --continue/--resume).
// useRef(fn()) evaluates fn() on every render and discards the result after
// mount — getInitialPasteId walks all messages + regex-scans text blocks,
// so guard with a lazy-init pattern to run it exactly once.
- const nextPasteIdRef = useRef(-1);
+ const nextPasteIdRef = useRef(-1)
if (nextPasteIdRef.current === -1) {
- nextPasteIdRef.current = getInitialPasteId(messages);
+ nextPasteIdRef.current = getInitialPasteId(messages)
}
// Armed by onImagePaste; if the very next keystroke is a non-space
// printable, inputFilter prepends a space before it. Any other input
// (arrow, escape, backspace, paste, space) disarms without inserting.
- const pendingSpaceAfterPillRef = useRef(false);
- const [showTeamsDialog, setShowTeamsDialog] = useState(false);
- const [showBridgeDialog, setShowBridgeDialog] = useState(false);
- const [teammateFooterIndex, setTeammateFooterIndex] = useState(0);
+ const pendingSpaceAfterPillRef = useRef(false)
+
+ const [showTeamsDialog, setShowTeamsDialog] = useState(false)
+ const [showBridgeDialog, setShowBridgeDialog] = useState(false)
+ const [teammateFooterIndex, setTeammateFooterIndex] = useState(0)
// -1 sentinel: tasks pill is selected but no specific agent row is selected yet.
// First ↓ selects the pill, second ↓ moves to row 0. Prevents double-select
// of pill + row when both bg tasks (pill) and forked agents (rows) are visible.
- const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex);
- const setCoordinatorTaskIndex = useCallback((v: number | ((prev: number) => number)) => setAppState(prev => {
- const next = typeof v === 'function' ? v(prev.coordinatorTaskIndex) : v;
- if (next === prev.coordinatorTaskIndex) return prev;
- return {
- ...prev,
- coordinatorTaskIndex: next
- };
- }), [setAppState]);
- const coordinatorTaskCount = useCoordinatorTaskCount();
+ const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex)
+ const setCoordinatorTaskIndex = useCallback(
+ (v: number | ((prev: number) => number)) =>
+ setAppState(prev => {
+ const next = typeof v === 'function' ? v(prev.coordinatorTaskIndex) : v
+ if (next === prev.coordinatorTaskIndex) return prev
+ return { ...prev, coordinatorTaskIndex: next }
+ }),
+ [setAppState],
+ )
+ const coordinatorTaskCount = useCoordinatorTaskCount()
// The pill (BackgroundTaskStatus) only renders when non-local_agent bg tasks
// exist. When only local_agent tasks are running (coordinator/fork mode), the
// pill is absent, so the -1 sentinel would leave nothing visually selected.
// In that case, skip -1 and treat 0 as the minimum selectable index.
- const hasBgTaskPill = useMemo(() => Object.values(tasks).some(t => isBackgroundTask(t) && !((process.env.USER_TYPE) === 'ant' && isPanelAgentTask(t))), [tasks]);
- const minCoordinatorIndex = hasBgTaskPill ? -1 : 0;
+ const hasBgTaskPill = useMemo(
+ () =>
+ Object.values(tasks).some(
+ t =>
+ isBackgroundTask(t) &&
+ !(process.env.USER_TYPE === 'ant' && isPanelAgentTask(t)),
+ ),
+ [tasks],
+ )
+ const minCoordinatorIndex = hasBgTaskPill ? -1 : 0
// Clamp index when tasks complete and the list shrinks beneath the cursor
useEffect(() => {
if (coordinatorTaskIndex >= coordinatorTaskCount) {
- setCoordinatorTaskIndex(Math.max(minCoordinatorIndex, coordinatorTaskCount - 1));
+ setCoordinatorTaskIndex(
+ Math.max(minCoordinatorIndex, coordinatorTaskCount - 1),
+ )
} else if (coordinatorTaskIndex < minCoordinatorIndex) {
- setCoordinatorTaskIndex(minCoordinatorIndex);
+ setCoordinatorTaskIndex(minCoordinatorIndex)
}
- }, [coordinatorTaskCount, coordinatorTaskIndex, minCoordinatorIndex]);
- const [isPasting, setIsPasting] = useState(false);
- const [isExternalEditorActive, setIsExternalEditorActive] = useState(false);
- const [showModelPicker, setShowModelPicker] = useState(false);
- const [showQuickOpen, setShowQuickOpen] = useState(false);
- const [showGlobalSearch, setShowGlobalSearch] = useState(false);
- const [showHistoryPicker, setShowHistoryPicker] = useState(false);
- const [showFastModePicker, setShowFastModePicker] = useState(false);
- const [showThinkingToggle, setShowThinkingToggle] = useState(false);
- const [showAutoModeOptIn, setShowAutoModeOptIn] = useState(false);
- const [previousModeBeforeAuto, setPreviousModeBeforeAuto] = useState(null);
- const autoModeOptInTimeoutRef = useRef(null);
+ }, [coordinatorTaskCount, coordinatorTaskIndex, minCoordinatorIndex])
+ const [isPasting, setIsPasting] = useState(false)
+ const [isExternalEditorActive, setIsExternalEditorActive] = useState(false)
+ const [showModelPicker, setShowModelPicker] = useState(false)
+ const [showQuickOpen, setShowQuickOpen] = useState(false)
+ const [showGlobalSearch, setShowGlobalSearch] = useState(false)
+ const [showHistoryPicker, setShowHistoryPicker] = useState(false)
+ const [showFastModePicker, setShowFastModePicker] = useState(false)
+ const [showThinkingToggle, setShowThinkingToggle] = useState(false)
+ const [showAutoModeOptIn, setShowAutoModeOptIn] = useState(false)
+ const [previousModeBeforeAuto, setPreviousModeBeforeAuto] =
+ useState(null)
+ const autoModeOptInTimeoutRef = useRef(null)
// Check if cursor is on the first line of input
const isCursorOnFirstLine = useMemo(() => {
- const firstNewlineIndex = input.indexOf('\n');
+ const firstNewlineIndex = input.indexOf('\n')
if (firstNewlineIndex === -1) {
- return true; // No newlines, cursor is always on first line
+ return true // No newlines, cursor is always on first line
}
- return cursorOffset <= firstNewlineIndex;
- }, [input, cursorOffset]);
+ return cursorOffset <= firstNewlineIndex
+ }, [input, cursorOffset])
+
const isCursorOnLastLine = useMemo(() => {
- const lastNewlineIndex = input.lastIndexOf('\n');
+ const lastNewlineIndex = input.lastIndexOf('\n')
if (lastNewlineIndex === -1) {
- return true; // No newlines, cursor is always on last line
+ return true // No newlines, cursor is always on last line
}
- return cursorOffset > lastNewlineIndex;
- }, [input, cursorOffset]);
+ return cursorOffset > lastNewlineIndex
+ }, [input, cursorOffset])
// Derive team info from teamContext (no filesystem I/O needed)
// A session can only lead one team at a time
const cachedTeams: TeamSummary[] = useMemo(() => {
- if (!isAgentSwarmsEnabled()) return [];
+ if (!isAgentSwarmsEnabled()) return []
// In-process mode uses Shift+Down/Up navigation instead of footer menu
- if (isInProcessEnabled()) return [];
+ if (isInProcessEnabled()) return []
if (!teamContext) {
- return [];
+ return []
}
- const teammateCount = count(Object.values(teamContext.teammates), t => t.name !== 'team-lead');
- return [{
- name: teamContext.teamName,
- memberCount: teammateCount,
- runningCount: 0,
- idleCount: 0
- }];
- }, [teamContext]);
+ const teammateCount = count(
+ Object.values(teamContext.teammates),
+ t => t.name !== 'team-lead',
+ )
+ return [
+ {
+ name: teamContext.teamName,
+ memberCount: teammateCount,
+ runningCount: 0,
+ idleCount: 0,
+ },
+ ]
+ }, [teamContext])
// ─── Footer pill navigation ─────────────────────────────────────────────
// Which pills render below the input box. Order here IS the nav order
// (down/right = forward, up/left = back). Selection lives in AppState so
// pills rendered outside PromptInput (CompanionSprite) can read focus.
- const runningTaskCount = useMemo(() => count(Object.values(tasks), t => t.status === 'running'), [tasks]);
+ const runningTaskCount = useMemo(
+ () => count(Object.values(tasks), t => t.status === 'running'),
+ [tasks],
+ )
// Panel shows retained-completed agents too (getVisibleAgentTasks), so the
// pill must stay navigable whenever the panel has rows — not just when
// something is running.
- const tasksFooterVisible = (runningTaskCount > 0 || (process.env.USER_TYPE) === 'ant' && coordinatorTaskCount > 0) && !shouldHideTasksFooter(tasks, showSpinnerTree);
- const teamsFooterVisible = cachedTeams.length > 0;
- const footerItems = useMemo(() => [tasksFooterVisible && 'tasks', tmuxFooterVisible && 'tmux', bagelFooterVisible && 'bagel', teamsFooterVisible && 'teams', bridgeFooterVisible && 'bridge', companionFooterVisible && 'companion'].filter(Boolean) as FooterItem[], [tasksFooterVisible, tmuxFooterVisible, bagelFooterVisible, teamsFooterVisible, bridgeFooterVisible, companionFooterVisible]);
+ const tasksFooterVisible =
+ (runningTaskCount > 0 ||
+ (process.env.USER_TYPE === 'ant' && coordinatorTaskCount > 0)) &&
+ !shouldHideTasksFooter(tasks, showSpinnerTree)
+ const teamsFooterVisible = cachedTeams.length > 0
+
+ const footerItems = useMemo(
+ () =>
+ [
+ tasksFooterVisible && 'tasks',
+ tmuxFooterVisible && 'tmux',
+ bagelFooterVisible && 'bagel',
+ teamsFooterVisible && 'teams',
+ bridgeFooterVisible && 'bridge',
+ companionFooterVisible && 'companion',
+ ].filter(Boolean) as FooterItem[],
+ [
+ tasksFooterVisible,
+ tmuxFooterVisible,
+ bagelFooterVisible,
+ teamsFooterVisible,
+ bridgeFooterVisible,
+ companionFooterVisible,
+ ],
+ )
// Effective selection: null if the selected pill stopped rendering (bridge
// disconnected, task finished). The derivation makes the UI correct
// immediately; the useEffect below clears the raw state so it doesn't
// resurrect when the same pill reappears (new task starts → focus stolen).
- const rawFooterSelection = useAppState(s => s.footerSelection);
- const footerItemSelected = rawFooterSelection && footerItems.includes(rawFooterSelection) ? rawFooterSelection : null;
+ const rawFooterSelection = useAppState(s => s.footerSelection)
+ const footerItemSelected =
+ rawFooterSelection && footerItems.includes(rawFooterSelection)
+ ? rawFooterSelection
+ : null
+
useEffect(() => {
if (rawFooterSelection && !footerItemSelected) {
- setAppState(prev => prev.footerSelection === null ? prev : {
- ...prev,
- footerSelection: null
- });
+ setAppState(prev =>
+ prev.footerSelection === null
+ ? prev
+ : { ...prev, footerSelection: null },
+ )
}
- }, [rawFooterSelection, footerItemSelected, setAppState]);
- const tasksSelected = footerItemSelected === 'tasks';
- const tmuxSelected = footerItemSelected === 'tmux';
- const bagelSelected = footerItemSelected === 'bagel';
- const teamsSelected = footerItemSelected === 'teams';
- const bridgeSelected = footerItemSelected === 'bridge';
+ }, [rawFooterSelection, footerItemSelected, setAppState])
+
+ const tasksSelected = footerItemSelected === 'tasks'
+ const tmuxSelected = footerItemSelected === 'tmux'
+ const bagelSelected = footerItemSelected === 'bagel'
+ const teamsSelected = footerItemSelected === 'teams'
+ const bridgeSelected = footerItemSelected === 'bridge'
+
function selectFooterItem(item: FooterItem | null): void {
- setAppState(prev => prev.footerSelection === item ? prev : {
- ...prev,
- footerSelection: item
- });
+ setAppState(prev =>
+ prev.footerSelection === item ? prev : { ...prev, footerSelection: item },
+ )
if (item === 'tasks') {
- setTeammateFooterIndex(0);
- setCoordinatorTaskIndex(minCoordinatorIndex);
+ setTeammateFooterIndex(0)
+ setCoordinatorTaskIndex(minCoordinatorIndex)
}
}
// delta: +1 = down/right, -1 = up/left. Returns true if nav happened
// (including deselecting at the start), false if at a boundary.
function navigateFooter(delta: 1 | -1, exitAtStart = false): boolean {
- const idx = footerItemSelected ? footerItems.indexOf(footerItemSelected) : -1;
- const next = footerItems[idx + delta];
+ const idx = footerItemSelected
+ ? footerItems.indexOf(footerItemSelected)
+ : -1
+ const next = footerItems[idx + delta]
if (next) {
- selectFooterItem(next);
- return true;
+ selectFooterItem(next)
+ return true
}
if (delta < 0 && exitAtStart) {
- selectFooterItem(null);
- return true;
+ selectFooterItem(null)
+ return true
}
- return false;
+ return false
}
// Prompt suggestion hook - reads suggestions generated by forked agent in query loop
@@ -510,96 +714,159 @@ function PromptInput({
suggestion: promptSuggestion,
markAccepted,
logOutcomeAtSubmission,
- markShown
+ markShown,
} = usePromptSuggestion({
inputValue: input,
- isAssistantResponding: isLoading
- });
- const displayedValue = useMemo(() => isSearchingHistory && historyMatch ? getValueFromInput(typeof historyMatch === 'string' ? historyMatch : historyMatch.display) : input, [isSearchingHistory, historyMatch, input]);
- const thinkTriggers = useMemo(() => findThinkingTriggerPositions(displayedValue), [displayedValue]);
- const ultraplanSessionUrl = useAppState(s => s.ultraplanSessionUrl);
- const ultraplanLaunching = useAppState(s => s.ultraplanLaunching);
- const ultraplanTriggers = useMemo(() => feature('ULTRAPLAN') && !ultraplanSessionUrl && !ultraplanLaunching ? findUltraplanTriggerPositions(displayedValue) : [], [displayedValue, ultraplanSessionUrl, ultraplanLaunching]);
- const ultrareviewTriggers = useMemo(() => isUltrareviewEnabled() ? findUltrareviewTriggerPositions(displayedValue) : [], [displayedValue]);
- const btwTriggers = useMemo(() => findBtwTriggerPositions(displayedValue), [displayedValue]);
- const buddyTriggers = useMemo(() => findBuddyTriggerPositions(displayedValue), [displayedValue]);
+ isAssistantResponding: isLoading,
+ })
+
+ const displayedValue = useMemo(
+ () =>
+ isSearchingHistory && historyMatch
+ ? getValueFromInput(
+ typeof historyMatch === 'string'
+ ? historyMatch
+ : historyMatch.display,
+ )
+ : input,
+ [isSearchingHistory, historyMatch, input],
+ )
+
+ const thinkTriggers = useMemo(
+ () => findThinkingTriggerPositions(displayedValue),
+ [displayedValue],
+ )
+
+ const ultraplanSessionUrl = useAppState(s => s.ultraplanSessionUrl)
+ const ultraplanLaunching = useAppState(s => s.ultraplanLaunching)
+ const ultraplanTriggers = useMemo(
+ () =>
+ feature('ULTRAPLAN') && !ultraplanSessionUrl && !ultraplanLaunching
+ ? findUltraplanTriggerPositions(displayedValue)
+ : [],
+ [displayedValue, ultraplanSessionUrl, ultraplanLaunching],
+ )
+
+ const ultrareviewTriggers = useMemo(
+ () =>
+ isUltrareviewEnabled()
+ ? findUltrareviewTriggerPositions(displayedValue)
+ : [],
+ [displayedValue],
+ )
+
+ const btwTriggers = useMemo(
+ () => findBtwTriggerPositions(displayedValue),
+ [displayedValue],
+ )
+
+ const buddyTriggers = useMemo(
+ () => findBuddyTriggerPositions(displayedValue),
+ [displayedValue],
+ )
+
const slashCommandTriggers = useMemo(() => {
- const positions = findSlashCommandPositions(displayedValue);
+ const positions = findSlashCommandPositions(displayedValue)
// Only highlight valid commands
return positions.filter(pos => {
- const commandName = displayedValue.slice(pos.start + 1, pos.end); // +1 to skip "/"
- return hasCommand(commandName, commands);
- });
- }, [displayedValue, commands]);
- const tokenBudgetTriggers = useMemo(() => feature('TOKEN_BUDGET') ? findTokenBudgetPositions(displayedValue) : [], [displayedValue]);
- const knownChannelsVersion = useSyncExternalStore(subscribeKnownChannels, getKnownChannelsVersion);
- const slackChannelTriggers = useMemo(() => hasSlackMcpServer(store.getState().mcp.clients) ? findSlackChannelPositions(displayedValue) : [],
- // eslint-disable-next-line react-hooks/exhaustive-deps -- store is a stable ref
- [displayedValue, knownChannelsVersion]);
+ const commandName = displayedValue.slice(pos.start + 1, pos.end) // +1 to skip "/"
+ return hasCommand(commandName, commands)
+ })
+ }, [displayedValue, commands])
+
+ const tokenBudgetTriggers = useMemo(
+ () =>
+ feature('TOKEN_BUDGET') ? findTokenBudgetPositions(displayedValue) : [],
+ [displayedValue],
+ )
+
+ const knownChannelsVersion = useSyncExternalStore(
+ subscribeKnownChannels,
+ getKnownChannelsVersion,
+ )
+ const slackChannelTriggers = useMemo(
+ () =>
+ hasSlackMcpServer(store.getState().mcp.clients)
+ ? findSlackChannelPositions(displayedValue)
+ : [],
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- store is a stable ref
+ [displayedValue, knownChannelsVersion],
+ )
// Find @name mentions and highlight with team member's color
const memberMentionHighlights = useMemo((): Array<{
- start: number;
- end: number;
- themeColor: keyof Theme;
+ start: number
+ end: number
+ themeColor: keyof Theme
}> => {
- if (!isAgentSwarmsEnabled()) return [];
- if (!teamContext?.teammates) return [];
+ if (!isAgentSwarmsEnabled()) return []
+ if (!teamContext?.teammates) return []
+
const highlights: Array<{
- start: number;
- end: number;
- themeColor: keyof Theme;
- }> = [];
- const members = teamContext.teammates;
- if (!members) return highlights;
+ start: number
+ end: number
+ themeColor: keyof Theme
+ }> = []
+ const members = teamContext.teammates
+ if (!members) return highlights
// Find all @name patterns in the input
- const regex = /(^|\s)@([\w-]+)/g;
- const memberValues = Object.values(members);
- let match;
+ const regex = /(^|\s)@([\w-]+)/g
+ const memberValues = Object.values(members)
+ let match
while ((match = regex.exec(displayedValue)) !== null) {
- const leadingSpace = match[1] ?? '';
- const nameStart = match.index + leadingSpace.length;
- const fullMatch = match[0].trimStart();
- const name = match[2];
+ const leadingSpace = match[1] ?? ''
+ const nameStart = match.index + leadingSpace.length
+ const fullMatch = match[0].trimStart()
+ const name = match[2]
// Check if this name matches a team member
- const member = memberValues.find(t => t.name === name);
+ const member = memberValues.find(t => t.name === name)
if (member?.color) {
- const themeColor = AGENT_COLOR_TO_THEME_COLOR[member.color as AgentColorName];
+ const themeColor =
+ AGENT_COLOR_TO_THEME_COLOR[member.color as AgentColorName]
if (themeColor) {
highlights.push({
start: nameStart,
end: nameStart + fullMatch.length,
- themeColor
- });
+ themeColor,
+ })
}
}
}
- return highlights;
- }, [displayedValue, teamContext]);
- const imageRefPositions = useMemo(() => parseReferences(displayedValue).filter(r => r.match.startsWith('[Image')).map(r => ({
- start: r.index,
- end: r.index + r.match.length
- })), [displayedValue]);
+ return highlights
+ }, [displayedValue, teamContext])
+
+ const imageRefPositions = useMemo(
+ () =>
+ parseReferences(displayedValue)
+ .filter(r => r.match.startsWith('[Image'))
+ .map(r => ({ start: r.index, end: r.index + r.match.length })),
+ [displayedValue],
+ )
// chip.start is the "selected" state: the inverted chip IS the cursor.
// chip.end stays a normal position so you can park the cursor right after
// `]` like any other character.
- const cursorAtImageChip = imageRefPositions.some(r => r.start === cursorOffset);
+ const cursorAtImageChip = imageRefPositions.some(
+ r => r.start === cursorOffset,
+ )
// up/down movement or a fullscreen click can land the cursor strictly
// inside a chip; snap to the nearer boundary so it's never editable
// char-by-char.
useEffect(() => {
- const inside = imageRefPositions.find(r => cursorOffset > r.start && cursorOffset < r.end);
+ const inside = imageRefPositions.find(
+ r => cursorOffset > r.start && cursorOffset < r.end,
+ )
if (inside) {
- const mid = (inside.start + inside.end) / 2;
- setCursorOffset(cursorOffset < mid ? inside.start : inside.end);
+ const mid = (inside.start + inside.end) / 2
+ setCursorOffset(cursorOffset < mid ? inside.start : inside.end)
}
- }, [cursorOffset, imageRefPositions, setCursorOffset]);
+ }, [cursorOffset, imageRefPositions, setCursorOffset])
+
const combinedHighlights = useMemo((): TextHighlight[] => {
- const highlights: TextHighlight[] = [];
+ const highlights: TextHighlight[] = []
// Invert the [Image #N] chip when the cursor is at chip.start (the
// "selected" state) so backspace-to-delete is visually obvious.
@@ -610,17 +877,18 @@ function PromptInput({
end: ref.end,
color: undefined,
inverse: true,
- priority: 8
- });
+ priority: 8,
+ })
}
}
+
if (isSearchingHistory && historyMatch && !historyFailedMatch) {
highlights.push({
start: cursorOffset,
end: cursorOffset + historyQuery.length,
color: 'warning',
- priority: 20
- });
+ priority: 20,
+ })
}
// Add "btw" highlighting (solid yellow)
@@ -629,8 +897,8 @@ function PromptInput({
start: trigger.start,
end: trigger.end,
color: 'warning',
- priority: 15
- });
+ priority: 15,
+ })
}
// Add /command highlighting (blue)
@@ -639,8 +907,8 @@ function PromptInput({
start: trigger.start,
end: trigger.end,
color: 'suggestion',
- priority: 5
- });
+ priority: 5,
+ })
}
// Add token budget highlighting (blue)
@@ -649,16 +917,17 @@ function PromptInput({
start: trigger.start,
end: trigger.end,
color: 'suggestion',
- priority: 5
- });
+ priority: 5,
+ })
}
+
for (const trigger of slackChannelTriggers) {
highlights.push({
start: trigger.start,
end: trigger.end,
color: 'suggestion',
- priority: 5
- });
+ priority: 5,
+ })
}
// Add @name highlighting with team member's color
@@ -667,8 +936,8 @@ function PromptInput({
start: mention.start,
end: mention.end,
color: mention.themeColor,
- priority: 5
- });
+ priority: 5,
+ })
}
// Dim interim voice dictation text
@@ -678,8 +947,8 @@ function PromptInput({
end: voiceInterimRange.end,
color: undefined,
dimColor: true,
- priority: 1
- });
+ priority: 1,
+ })
}
// Rainbow highlighting for ultrathink keyword (per-character cycling colors)
@@ -691,8 +960,8 @@ function PromptInput({
end: i + 1,
color: getRainbowColor(i - trigger.start),
shimmerColor: getRainbowColor(i - trigger.start, true),
- priority: 10
- });
+ priority: 10,
+ })
}
}
}
@@ -706,8 +975,8 @@ function PromptInput({
end: i + 1,
color: getRainbowColor(i - trigger.start),
shimmerColor: getRainbowColor(i - trigger.start, true),
- priority: 10
- });
+ priority: 10,
+ })
}
}
}
@@ -720,8 +989,8 @@ function PromptInput({
end: i + 1,
color: getRainbowColor(i - trigger.start),
shimmerColor: getRainbowColor(i - trigger.start, true),
- priority: 10
- });
+ priority: 10,
+ })
}
}
@@ -733,16 +1002,33 @@ function PromptInput({
end: i + 1,
color: getRainbowColor(i - trigger.start),
shimmerColor: getRainbowColor(i - trigger.start, true),
- priority: 10
- });
+ priority: 10,
+ })
}
}
- return highlights;
- }, [isSearchingHistory, historyQuery, historyMatch, historyFailedMatch, cursorOffset, btwTriggers, imageRefPositions, memberMentionHighlights, slashCommandTriggers, tokenBudgetTriggers, slackChannelTriggers, displayedValue, voiceInterimRange, thinkTriggers, ultraplanTriggers, ultrareviewTriggers, buddyTriggers]);
- const {
- addNotification,
- removeNotification
- } = useNotifications();
+
+ return highlights
+ }, [
+ isSearchingHistory,
+ historyQuery,
+ historyMatch,
+ historyFailedMatch,
+ cursorOffset,
+ btwTriggers,
+ imageRefPositions,
+ memberMentionHighlights,
+ slashCommandTriggers,
+ tokenBudgetTriggers,
+ slackChannelTriggers,
+ displayedValue,
+ voiceInterimRange,
+ thinkTriggers,
+ ultraplanTriggers,
+ ultrareviewTriggers,
+ buddyTriggers,
+ ])
+
+ const { addNotification, removeNotification } = useNotifications()
// Show ultrathink notification
useEffect(() => {
@@ -751,364 +1037,463 @@ function PromptInput({
key: 'ultrathink-active',
text: 'Effort set to high for this turn',
priority: 'immediate',
- timeoutMs: 5000
- });
+ timeoutMs: 5000,
+ })
} else {
- removeNotification('ultrathink-active');
+ removeNotification('ultrathink-active')
}
- }, [addNotification, removeNotification, thinkTriggers.length]);
+ }, [addNotification, removeNotification, thinkTriggers.length])
+
useEffect(() => {
if (feature('ULTRAPLAN') && ultraplanTriggers.length) {
addNotification({
key: 'ultraplan-active',
text: 'This prompt will launch an ultraplan session in Claude Code on the web',
priority: 'immediate',
- timeoutMs: 5000
- });
+ timeoutMs: 5000,
+ })
} else {
- removeNotification('ultraplan-active');
+ removeNotification('ultraplan-active')
}
- }, [addNotification, removeNotification, ultraplanTriggers.length]);
+ }, [addNotification, removeNotification, ultraplanTriggers.length])
+
useEffect(() => {
if (isUltrareviewEnabled() && ultrareviewTriggers.length) {
addNotification({
key: 'ultrareview-active',
text: 'Run /ultrareview after Claude finishes to review these changes in the cloud',
priority: 'immediate',
- timeoutMs: 5000
- });
+ timeoutMs: 5000,
+ })
}
- }, [addNotification, ultrareviewTriggers.length]);
+ }, [addNotification, ultrareviewTriggers.length])
// Track input length for stash hint
- const prevInputLengthRef = useRef(input.length);
- const peakInputLengthRef = useRef(input.length);
+ const prevInputLengthRef = useRef(input.length)
+ const peakInputLengthRef = useRef(input.length)
// Dismiss stash hint when user makes any input change
const dismissStashHint = useCallback(() => {
- removeNotification('stash-hint');
- }, [removeNotification]);
+ removeNotification('stash-hint')
+ }, [removeNotification])
// Show stash hint when user gradually clears substantial input
useEffect(() => {
- const prevLength = prevInputLengthRef.current;
- const peakLength = peakInputLengthRef.current;
- const currentLength = input.length;
- prevInputLengthRef.current = currentLength;
+ const prevLength = prevInputLengthRef.current
+ const peakLength = peakInputLengthRef.current
+ const currentLength = input.length
+ prevInputLengthRef.current = currentLength
// Update peak when input grows
if (currentLength > peakLength) {
- peakInputLengthRef.current = currentLength;
- return;
+ peakInputLengthRef.current = currentLength
+ return
}
// Reset state when input is empty
if (currentLength === 0) {
- peakInputLengthRef.current = 0;
- return;
+ peakInputLengthRef.current = 0
+ return
}
// Detect gradual clear: peak was high, current is low, but this wasn't a single big jump
// (rapid clears like esc-esc go from 20+ to 0 in one step)
- const clearedSubstantialInput = peakLength >= 20 && currentLength <= 5;
- const wasRapidClear = prevLength >= 20 && currentLength <= 5;
+ const clearedSubstantialInput = peakLength >= 20 && currentLength <= 5
+ const wasRapidClear = prevLength >= 20 && currentLength <= 5
+
if (clearedSubstantialInput && !wasRapidClear) {
- const config = getGlobalConfig();
+ const config = getGlobalConfig()
if (!config.hasUsedStash) {
addNotification({
key: 'stash-hint',
- jsx:
+ jsx: (
+
Tip:{' '}
-
- ,
+
+
+ ),
priority: 'immediate',
- timeoutMs: FOOTER_TEMPORARY_STATUS_TIMEOUT
- });
+ timeoutMs: FOOTER_TEMPORARY_STATUS_TIMEOUT,
+ })
}
- peakInputLengthRef.current = currentLength;
+ peakInputLengthRef.current = currentLength
}
- }, [input.length, addNotification]);
+ }, [input.length, addNotification])
// Initialize input buffer for undo functionality
- const {
- pushToBuffer,
- undo,
- canUndo,
- clearBuffer
- } = useInputBuffer({
+ const { pushToBuffer, undo, canUndo, clearBuffer } = useInputBuffer({
maxBufferSize: 50,
- debounceMs: 1000
- });
+ debounceMs: 1000,
+ })
+
useMaybeTruncateInput({
input,
pastedContents,
onInputChange: trackAndSetInput,
setCursorOffset,
- setPastedContents
- });
+ setPastedContents,
+ })
+
const defaultPlaceholder = usePromptInputPlaceholder({
input,
submitCount,
- viewingAgentName
- });
- const onChange = useCallback((value: string) => {
- if (value === '?') {
- logEvent('tengu_help_toggled', {});
- setHelpOpen(v => !v);
- return;
- }
- setHelpOpen(false);
+ viewingAgentName,
+ })
- // Dismiss stash hint when user makes any input change
- dismissStashHint();
-
- // Cancel any pending prompt suggestion and speculation when user types
- abortPromptSuggestion();
- abortSpeculation(setAppState);
-
- // Check if this is a single character insertion at the start
- const isSingleCharInsertion = value.length === input.length + 1;
- const insertedAtStart = cursorOffset === 0;
- const mode = getModeFromInput(value);
- if (insertedAtStart && mode !== 'prompt') {
- if (isSingleCharInsertion) {
- onModeChange(mode);
- return;
+ const onChange = useCallback(
+ (value: string) => {
+ if (value === '?') {
+ logEvent('tengu_help_toggled', {})
+ setHelpOpen(v => !v)
+ return
}
- // Multi-char insertion into empty input (e.g. tab-accepting "! gcloud auth login")
- if (input.length === 0) {
- onModeChange(mode);
- const valueWithoutMode = getValueFromInput(value).replaceAll('\t', ' ');
- pushToBuffer(input, cursorOffset, pastedContents);
- trackAndSetInput(valueWithoutMode);
- setCursorOffset(valueWithoutMode.length);
- return;
+ setHelpOpen(false)
+
+ // Dismiss stash hint when user makes any input change
+ dismissStashHint()
+
+ // Cancel any pending prompt suggestion and speculation when user types
+ abortPromptSuggestion()
+ abortSpeculation(setAppState)
+
+ // Check if this is a single character insertion at the start
+ const isSingleCharInsertion = value.length === input.length + 1
+ const insertedAtStart = cursorOffset === 0
+ const mode = getModeFromInput(value)
+
+ if (insertedAtStart && mode !== 'prompt') {
+ if (isSingleCharInsertion) {
+ onModeChange(mode)
+ return
+ }
+ // Multi-char insertion into empty input (e.g. tab-accepting "! gcloud auth login")
+ if (input.length === 0) {
+ onModeChange(mode)
+ const valueWithoutMode = getValueFromInput(value).replaceAll(
+ '\t',
+ ' ',
+ )
+ pushToBuffer(input, cursorOffset, pastedContents)
+ trackAndSetInput(valueWithoutMode)
+ setCursorOffset(valueWithoutMode.length)
+ return
+ }
}
- }
- const processedValue = value.replaceAll('\t', ' ');
- // Push current state to buffer before making changes
- if (input !== processedValue) {
- pushToBuffer(input, cursorOffset, pastedContents);
- }
+ const processedValue = value.replaceAll('\t', ' ')
+
+ // Push current state to buffer before making changes
+ if (input !== processedValue) {
+ pushToBuffer(input, cursorOffset, pastedContents)
+ }
+
+ // Deselect footer items when user types
+ setAppState(prev =>
+ prev.footerSelection === null
+ ? prev
+ : { ...prev, footerSelection: null },
+ )
+
+ trackAndSetInput(processedValue)
+ },
+ [
+ trackAndSetInput,
+ onModeChange,
+ input,
+ cursorOffset,
+ pushToBuffer,
+ pastedContents,
+ dismissStashHint,
+ setAppState,
+ ],
+ )
- // Deselect footer items when user types
- setAppState(prev => prev.footerSelection === null ? prev : {
- ...prev,
- footerSelection: null
- });
- trackAndSetInput(processedValue);
- }, [trackAndSetInput, onModeChange, input, cursorOffset, pushToBuffer, pastedContents, dismissStashHint, setAppState]);
const {
resetHistory,
onHistoryUp,
onHistoryDown,
dismissSearchHint,
- historyIndex
- } = useArrowKeyHistory((value: string, historyMode: HistoryMode, pastedContents: Record) => {
- onChange(value);
- onModeChange(historyMode);
- setPastedContents(pastedContents);
- }, input, pastedContents, setCursorOffset, mode);
+ historyIndex,
+ } = useArrowKeyHistory(
+ (
+ value: string,
+ historyMode: HistoryMode,
+ pastedContents: Record,
+ ) => {
+ onChange(value)
+ onModeChange(historyMode)
+ setPastedContents(pastedContents)
+ },
+ input,
+ pastedContents,
+ setCursorOffset,
+ mode,
+ )
// Dismiss search hint when user starts searching
useEffect(() => {
if (isSearchingHistory) {
- dismissSearchHint();
+ dismissSearchHint()
}
- }, [isSearchingHistory, dismissSearchHint]);
+ }, [isSearchingHistory, dismissSearchHint])
// Only use history navigation when there are 0 or 1 slash command suggestions.
// Footer nav is NOT here — when a pill is selected, TextInput focus=false so
// these never fire. The Footer keybinding context handles ↑/↓ instead.
function handleHistoryUp() {
if (suggestions.length > 1) {
- return;
+ return
}
// Only navigate history when cursor is on the first line.
// In multiline inputs, up arrow should move the cursor (handled by TextInput)
// and only trigger history when at the top of the input.
if (!isCursorOnFirstLine) {
- return;
+ return
}
// If there's an editable queued command, move it to the input for editing when UP is pressed
- const hasEditableCommand = queuedCommands.some(isQueuedCommandEditable);
+ const hasEditableCommand = queuedCommands.some(isQueuedCommandEditable)
if (hasEditableCommand) {
- void popAllCommandsFromQueue();
- return;
+ void popAllCommandsFromQueue()
+ return
}
- onHistoryUp();
+
+ onHistoryUp()
}
+
function handleHistoryDown() {
if (suggestions.length > 1) {
- return;
+ return
}
// Only navigate history/footer when cursor is on the last line.
// In multiline inputs, down arrow should move the cursor (handled by TextInput)
// and only trigger navigation when at the bottom of the input.
if (!isCursorOnLastLine) {
- return;
+ return
}
// At bottom of history → enter footer at first visible pill
if (onHistoryDown() && footerItems.length > 0) {
- const first = footerItems[0]!;
- selectFooterItem(first);
+ const first = footerItems[0]!
+ selectFooterItem(first)
if (first === 'tasks' && !getGlobalConfig().hasSeenTasksHint) {
- saveGlobalConfig(c => c.hasSeenTasksHint ? c : {
- ...c,
- hasSeenTasksHint: true
- });
+ saveGlobalConfig(c =>
+ c.hasSeenTasksHint ? c : { ...c, hasSeenTasksHint: true },
+ )
}
}
}
// Create a suggestions state directly - we'll sync it with useTypeahead later
const [suggestionsState, setSuggestionsStateRaw] = useState<{
- suggestions: SuggestionItem[];
- selectedSuggestion: number;
- commandArgumentHint?: string;
+ suggestions: SuggestionItem[]
+ selectedSuggestion: number
+ commandArgumentHint?: string
}>({
suggestions: [],
selectedSuggestion: -1,
- commandArgumentHint: undefined
- });
+ commandArgumentHint: undefined,
+ })
// Setter for suggestions state
- const setSuggestionsState = useCallback((updater: typeof suggestionsState | ((prev: typeof suggestionsState) => typeof suggestionsState)) => {
- setSuggestionsStateRaw(prev => typeof updater === 'function' ? updater(prev) : updater);
- }, []);
- const onSubmit = useCallback(async (inputParam: string, isSubmittingSlashCommand = false) => {
- inputParam = inputParam.trimEnd();
+ const setSuggestionsState = useCallback(
+ (
+ updater:
+ | typeof suggestionsState
+ | ((prev: typeof suggestionsState) => typeof suggestionsState),
+ ) => {
+ setSuggestionsStateRaw(prev =>
+ typeof updater === 'function' ? updater(prev) : updater,
+ )
+ },
+ [],
+ )
- // Don't submit if a footer indicator is being opened. Read fresh from
- // store — footer:openSelected calls selectFooterItem(null) then onSubmit
- // in the same tick, and the closure value hasn't updated yet. Apply the
- // same "still visible?" derivation as footerItemSelected so a stale
- // selection (pill disappeared) doesn't swallow Enter.
- const state = store.getState();
- if (state.footerSelection && footerItems.includes(state.footerSelection)) {
- return;
- }
+ const onSubmit = useCallback(
+ async (inputParam: string, isSubmittingSlashCommand = false) => {
+ inputParam = inputParam.trimEnd()
- // Enter in selection modes confirms selection (useBackgroundTaskNavigation).
- // BaseTextInput's useInput registers before that hook (child effects fire first),
- // so without this guard Enter would double-fire and auto-submit the suggestion.
- if (state.viewSelectionMode === 'selecting-agent') {
- return;
- }
-
- // Check for images early - we need this for suggestion logic below
- const hasImages = Object.values(pastedContents).some(c => c.type === 'image');
-
- // If input is empty OR matches the suggestion, submit it
- // But if there are images attached, don't auto-accept the suggestion -
- // the user wants to submit just the image(s).
- // Only in leader view — promptSuggestion is leader-context, not teammate.
- const suggestionText = promptSuggestionState.text;
- const inputMatchesSuggestion = inputParam.trim() === '' || inputParam === suggestionText;
- if (inputMatchesSuggestion && suggestionText && !hasImages && !state.viewingAgentTaskId) {
- // If speculation is active, inject messages immediately as they stream
- if (speculation.status === 'active') {
- markAccepted();
- // skipReset: resetSuggestion would abort the speculation before we accept it
- logOutcomeAtSubmission(suggestionText, {
- skipReset: true
- });
- void onSubmitProp(suggestionText, {
- setCursorOffset,
- clearBuffer,
- resetHistory
- }, {
- state: speculation,
- speculationSessionTimeSavedMs: speculationSessionTimeSavedMs,
- setAppState
- });
- return; // Skip normal query - speculation handled it
+ // Don't submit if a footer indicator is being opened. Read fresh from
+ // store — footer:openSelected calls selectFooterItem(null) then onSubmit
+ // in the same tick, and the closure value hasn't updated yet. Apply the
+ // same "still visible?" derivation as footerItemSelected so a stale
+ // selection (pill disappeared) doesn't swallow Enter.
+ const state = store.getState()
+ if (
+ state.footerSelection &&
+ footerItems.includes(state.footerSelection)
+ ) {
+ return
}
- // Regular suggestion acceptance (requires shownAt > 0)
- if (promptSuggestionState.shownAt > 0) {
- markAccepted();
- inputParam = suggestionText;
+ // Enter in selection modes confirms selection (useBackgroundTaskNavigation).
+ // BaseTextInput's useInput registers before that hook (child effects fire first),
+ // so without this guard Enter would double-fire and auto-submit the suggestion.
+ if (state.viewSelectionMode === 'selecting-agent') {
+ return
}
- }
- // Handle @name direct message
- if (isAgentSwarmsEnabled()) {
- const directMessage = parseDirectMemberMessage(inputParam);
- if (directMessage) {
- const result = await sendDirectMemberMessage(directMessage.recipientName, directMessage.message, teamContext, writeToMailbox);
- if (result.success) {
- addNotification({
- key: 'direct-message-sent',
- text: `Sent to @${result.recipientName}`,
- priority: 'immediate',
- timeoutMs: 3000
- });
- trackAndSetInput('');
- setCursorOffset(0);
- clearBuffer();
- resetHistory();
- return;
- } else if ('error' in result && result.error === 'no_team_context') {
- // No team context - fall through to normal prompt submission
- } else {
- // Unknown recipient - fall through to normal prompt submission
- // This allows e.g. "@utils explain this code" to be sent as a prompt
+ // Check for images early - we need this for suggestion logic below
+ const hasImages = Object.values(pastedContents).some(
+ c => c.type === 'image',
+ )
+
+ // If input is empty OR matches the suggestion, submit it
+ // But if there are images attached, don't auto-accept the suggestion -
+ // the user wants to submit just the image(s).
+ // Only in leader view — promptSuggestion is leader-context, not teammate.
+ const suggestionText = promptSuggestionState.text
+ const inputMatchesSuggestion =
+ inputParam.trim() === '' || inputParam === suggestionText
+ if (
+ inputMatchesSuggestion &&
+ suggestionText &&
+ !hasImages &&
+ !state.viewingAgentTaskId
+ ) {
+ // If speculation is active, inject messages immediately as they stream
+ if (speculation.status === 'active') {
+ markAccepted()
+ // skipReset: resetSuggestion would abort the speculation before we accept it
+ logOutcomeAtSubmission(suggestionText, { skipReset: true })
+
+ void onSubmitProp(
+ suggestionText,
+ {
+ setCursorOffset,
+ clearBuffer,
+ resetHistory,
+ },
+ {
+ state: speculation,
+ speculationSessionTimeSavedMs: speculationSessionTimeSavedMs,
+ setAppState,
+ },
+ )
+ return // Skip normal query - speculation handled it
+ }
+
+ // Regular suggestion acceptance (requires shownAt > 0)
+ if (promptSuggestionState.shownAt > 0) {
+ markAccepted()
+ inputParam = suggestionText
}
}
- }
- // Allow submission if there are images attached, even without text
- if (inputParam.trim() === '' && !hasImages) {
- return;
- }
+ // Handle @name direct message
+ if (isAgentSwarmsEnabled()) {
+ const directMessage = parseDirectMemberMessage(inputParam)
+ if (directMessage) {
+ const result = await sendDirectMemberMessage(
+ directMessage.recipientName,
+ directMessage.message,
+ teamContext,
+ writeToMailbox,
+ )
- // PromptInput UX: Check if suggestions dropdown is showing
- // For directory suggestions, allow submission (Tab is used for completion)
- const hasDirectorySuggestions = suggestionsState.suggestions.length > 0 && suggestionsState.suggestions.every(s => s.description === 'directory');
- if (suggestionsState.suggestions.length > 0 && !isSubmittingSlashCommand && !hasDirectorySuggestions) {
- logForDebugging(`[onSubmit] early return: suggestions showing (count=${suggestionsState.suggestions.length})`);
- return; // Don't submit, user needs to clear suggestions first
- }
+ if (result.success) {
+ addNotification({
+ key: 'direct-message-sent',
+ text: `Sent to @${result.recipientName}`,
+ priority: 'immediate',
+ timeoutMs: 3000,
+ })
+ trackAndSetInput('')
+ setCursorOffset(0)
+ clearBuffer()
+ resetHistory()
+ return
+ } else if (result.error === 'no_team_context') {
+ // No team context - fall through to normal prompt submission
+ } else {
+ // Unknown recipient - fall through to normal prompt submission
+ // This allows e.g. "@utils explain this code" to be sent as a prompt
+ }
+ }
+ }
- // Log suggestion outcome if one exists
- if (promptSuggestionState.text && promptSuggestionState.shownAt > 0) {
- logOutcomeAtSubmission(inputParam);
- }
+ // Allow submission if there are images attached, even without text
+ if (inputParam.trim() === '' && !hasImages) {
+ return
+ }
- // Clear stash hint notification on submit
- removeNotification('stash-hint');
+ // PromptInput UX: Check if suggestions dropdown is showing
+ // For directory suggestions, allow submission (Tab is used for completion)
+ const hasDirectorySuggestions =
+ suggestionsState.suggestions.length > 0 &&
+ suggestionsState.suggestions.every(s => s.description === 'directory')
- // Route input to viewed agent (in-process teammate or named local_agent).
- const activeAgent = getActiveAgentForInput(store.getState());
- if (activeAgent.type !== 'leader' && onAgentSubmit) {
- logEvent('tengu_transcript_input_to_teammate', {});
- await onAgentSubmit(inputParam, activeAgent.task, {
+ if (
+ suggestionsState.suggestions.length > 0 &&
+ !isSubmittingSlashCommand &&
+ !hasDirectorySuggestions
+ ) {
+ logForDebugging(
+ `[onSubmit] early return: suggestions showing (count=${suggestionsState.suggestions.length})`,
+ )
+ return // Don't submit, user needs to clear suggestions first
+ }
+
+ // Log suggestion outcome if one exists
+ if (promptSuggestionState.text && promptSuggestionState.shownAt > 0) {
+ logOutcomeAtSubmission(inputParam)
+ }
+
+ // Clear stash hint notification on submit
+ removeNotification('stash-hint')
+
+ // Route input to viewed agent (in-process teammate or named local_agent).
+ const activeAgent = getActiveAgentForInput(store.getState())
+ if (activeAgent.type !== 'leader' && onAgentSubmit) {
+ logEvent('tengu_transcript_input_to_teammate', {})
+ await onAgentSubmit(inputParam, activeAgent.task, {
+ setCursorOffset,
+ clearBuffer,
+ resetHistory,
+ })
+ return
+ }
+
+ // Normal leader submission
+ await onSubmitProp(inputParam, {
setCursorOffset,
clearBuffer,
- resetHistory
- });
- return;
- }
-
- // Normal leader submission
- await onSubmitProp(inputParam, {
- setCursorOffset,
+ resetHistory,
+ })
+ },
+ [
+ promptSuggestionState,
+ speculation,
+ speculationSessionTimeSavedMs,
+ teamContext,
+ store,
+ footerItems,
+ suggestionsState.suggestions,
+ onSubmitProp,
+ onAgentSubmit,
clearBuffer,
- resetHistory
- });
- }, [promptSuggestionState, speculation, speculationSessionTimeSavedMs, teamContext, store, footerItems, suggestionsState.suggestions, onSubmitProp, onAgentSubmit, clearBuffer, resetHistory, logOutcomeAtSubmission, setAppState, markAccepted, pastedContents, removeNotification]);
+ resetHistory,
+ logOutcomeAtSubmission,
+ setAppState,
+ markAccepted,
+ pastedContents,
+ removeNotification,
+ ],
+ )
+
const {
suggestions,
selectedSuggestion,
commandArgumentHint,
inlineGhostText,
- maxColumnWidth
+ maxColumnWidth,
} = useTypeahead({
commands,
onInputChange: trackAndSetInput,
@@ -1122,21 +1507,30 @@ function PromptInput({
suggestionsState,
suppressSuggestions: isSearchingHistory || historyIndex > 0,
markAccepted,
- onModeChange
- });
+ onModeChange,
+ })
// Track if prompt suggestion should be shown (computed later with terminal width).
// Hidden in teammate view — suggestion is leader-context only.
- const showPromptSuggestion = mode === 'prompt' && suggestions.length === 0 && promptSuggestion && !viewingAgentTaskId;
+ const showPromptSuggestion =
+ mode === 'prompt' &&
+ suggestions.length === 0 &&
+ promptSuggestion &&
+ !viewingAgentTaskId
if (showPromptSuggestion) {
- markShown();
+ markShown()
}
// If suggestion was generated but can't be shown due to timing, log suppression.
// Exclude teammate view: markShown() is gated above, so shownAt stays 0 there —
// but that's not a timing failure, the suggestion is valid when returning to leader.
- if (promptSuggestionState.text && !promptSuggestion && promptSuggestionState.shownAt === 0 && !viewingAgentTaskId) {
- logSuggestionSuppressed('timing', promptSuggestionState.text);
+ if (
+ promptSuggestionState.text &&
+ !promptSuggestion &&
+ promptSuggestionState.shownAt === 0 &&
+ !viewingAgentTaskId
+ ) {
+ logSuggestionSuppressed('timing', promptSuggestionState.text)
setAppState(prev => ({
...prev,
promptSuggestion: {
@@ -1144,42 +1538,47 @@ function PromptInput({
promptId: null,
shownAt: 0,
acceptedAt: 0,
- generationRequestId: null
- }
- }));
+ generationRequestId: null,
+ },
+ }))
}
- function onImagePaste(image: string, mediaType?: string, filename?: string, dimensions?: ImageDimensions, sourcePath?: string) {
- logEvent('tengu_paste_image', {});
- onModeChange('prompt');
- const pasteId = nextPasteIdRef.current++;
+
+ function onImagePaste(
+ image: string,
+ mediaType?: string,
+ filename?: string,
+ dimensions?: ImageDimensions,
+ sourcePath?: string,
+ ) {
+ logEvent('tengu_paste_image', {})
+ onModeChange('prompt')
+
+ const pasteId = nextPasteIdRef.current++
+
const newContent: PastedContent = {
id: pasteId,
type: 'image',
content: image,
- mediaType: mediaType || 'image/png',
- // default to PNG if not provided
+ mediaType: mediaType || 'image/png', // default to PNG if not provided
filename: filename || 'Pasted image',
dimensions,
- sourcePath
- };
+ sourcePath,
+ }
// Cache path immediately (fast) so links work on render
- cacheImagePath(newContent);
+ cacheImagePath(newContent)
// Store image to disk in background
- void storeImage(newContent);
+ void storeImage(newContent)
// Update UI
- setPastedContents(prev => ({
- ...prev,
- [pasteId]: newContent
- }));
+ setPastedContents(prev => ({ ...prev, [pasteId]: newContent }))
// Multi-image paste calls onImagePaste in a loop. If the ref is already
// armed, the previous pill's lazy space fires now (before this pill)
// rather than being lost.
- const prefix = pendingSpaceAfterPillRef.current ? ' ' : '';
- insertTextAtCursor(prefix + formatImageRef(pasteId));
- pendingSpaceAfterPillRef.current = true;
+ const prefix = pendingSpaceAfterPillRef.current ? ' ' : ''
+ insertTextAtCursor(prefix + formatImageRef(pasteId))
+ pendingSpaceAfterPillRef.current = true
}
// Prune images whose [Image #N] placeholder is no longer in the input text.
@@ -1187,224 +1586,260 @@ function PromptInput({
// the ref. onImagePaste batches setPastedContents + insertTextAtCursor in the
// same event, so this effect sees the placeholder already present.
useEffect(() => {
- const referencedIds = new Set(parseReferences(input).map(r => r.id));
+ const referencedIds = new Set(parseReferences(input).map(r => r.id))
setPastedContents(prev => {
- const orphaned = Object.values(prev).filter(c => c.type === 'image' && !referencedIds.has(c.id));
- if (orphaned.length === 0) return prev;
- const next = {
- ...prev
- };
- for (const img of orphaned) delete next[img.id];
- return next;
- });
- }, [input, setPastedContents]);
+ const orphaned = Object.values(prev).filter(
+ c => c.type === 'image' && !referencedIds.has(c.id),
+ )
+ if (orphaned.length === 0) return prev
+ const next = { ...prev }
+ for (const img of orphaned) delete next[img.id]
+ return next
+ })
+ }, [input, setPastedContents])
+
function onTextPaste(rawText: string) {
- pendingSpaceAfterPillRef.current = false;
+ pendingSpaceAfterPillRef.current = false
// Clean up pasted text - strip ANSI escape codes and normalize line endings and tabs
- let text = stripAnsi(rawText).replace(/\r/g, '\n').replaceAll('\t', ' ');
+ let text = stripAnsi(rawText).replace(/\r/g, '\n').replaceAll('\t', ' ')
// Match typed/auto-suggest: `!cmd` pasted into empty input enters bash mode.
if (input.length === 0) {
- const pastedMode = getModeFromInput(text);
+ const pastedMode = getModeFromInput(text)
if (pastedMode !== 'prompt') {
- onModeChange(pastedMode);
- text = getValueFromInput(text);
+ onModeChange(pastedMode)
+ text = getValueFromInput(text)
}
}
- const numLines = getPastedTextRefNumLines(text);
+
+ const numLines = getPastedTextRefNumLines(text)
// Limit the number of lines to show in the input
// If the overall layout is too high then Ink will repaint
// the entire terminal.
// The actual required height is dependent on the content, this
// is just an estimate.
- const maxLines = Math.min(rows - 10, 2);
+ const maxLines = Math.min(rows - 10, 2)
// Use special handling for long pasted text (>PASTE_THRESHOLD chars)
// or if it exceeds the number of lines we want to show
if (text.length > PASTE_THRESHOLD || numLines > maxLines) {
- const pasteId = nextPasteIdRef.current++;
+ const pasteId = nextPasteIdRef.current++
+
const newContent: PastedContent = {
id: pasteId,
type: 'text',
- content: text
- };
- setPastedContents(prev => ({
- ...prev,
- [pasteId]: newContent
- }));
- insertTextAtCursor(formatPastedTextRef(pasteId, numLines));
+ content: text,
+ }
+
+ setPastedContents(prev => ({ ...prev, [pasteId]: newContent }))
+
+ insertTextAtCursor(formatPastedTextRef(pasteId, numLines))
} else {
// For shorter pastes, just insert the text normally
- insertTextAtCursor(text);
+ insertTextAtCursor(text)
}
}
- const lazySpaceInputFilter = useCallback((input: string, key: Key): string => {
- if (!pendingSpaceAfterPillRef.current) return input;
- pendingSpaceAfterPillRef.current = false;
- if (isNonSpacePrintable(input, key)) return ' ' + input;
- return input;
- }, []);
+
+ const lazySpaceInputFilter = useCallback(
+ (input: string, key: Key): string => {
+ if (!pendingSpaceAfterPillRef.current) return input
+ pendingSpaceAfterPillRef.current = false
+ if (isNonSpacePrintable(input, key)) return ' ' + input
+ return input
+ },
+ [],
+ )
+
function insertTextAtCursor(text: string) {
// Push current state to buffer before inserting
- pushToBuffer(input, cursorOffset, pastedContents);
- const newInput = input.slice(0, cursorOffset) + text + input.slice(cursorOffset);
- trackAndSetInput(newInput);
- setCursorOffset(cursorOffset + text.length);
+ pushToBuffer(input, cursorOffset, pastedContents)
+
+ const newInput =
+ input.slice(0, cursorOffset) + text + input.slice(cursorOffset)
+ trackAndSetInput(newInput)
+ setCursorOffset(cursorOffset + text.length)
}
- const doublePressEscFromEmpty = useDoublePress(() => {}, () => onShowMessageSelector());
+
+ const doublePressEscFromEmpty = useDoublePress(
+ () => {},
+ () => onShowMessageSelector(),
+ )
// Function to get the queued command for editing. Returns true if commands were popped.
const popAllCommandsFromQueue = useCallback((): boolean => {
- const result = popAllEditable(input, cursorOffset);
+ const result = popAllEditable(input, cursorOffset)
if (!result) {
- return false;
+ return false
}
- trackAndSetInput(result.text);
- onModeChange('prompt'); // Always prompt mode for queued commands
- setCursorOffset(result.cursorOffset);
+
+ trackAndSetInput(result.text)
+ onModeChange('prompt') // Always prompt mode for queued commands
+ setCursorOffset(result.cursorOffset)
// Restore images from queued commands to pastedContents
if (result.images.length > 0) {
setPastedContents(prev => {
- const newContents = {
- ...prev
- };
+ const newContents = { ...prev }
for (const image of result.images) {
- newContents[image.id] = image;
+ newContents[image.id] = image
}
- return newContents;
- });
+ return newContents
+ })
}
- return true;
- }, [trackAndSetInput, onModeChange, input, cursorOffset, setPastedContents]);
+
+ return true
+ }, [trackAndSetInput, onModeChange, input, cursorOffset, setPastedContents])
// Insert the at-mentioned reference (the file and, optionally, a line range) when
// we receive an at-mentioned notification the IDE.
const onIdeAtMentioned = function (atMentioned: IDEAtMentioned) {
- logEvent('tengu_ext_at_mentioned', {});
- let atMentionedText: string;
- const relativePath = path.relative(getCwd(), atMentioned.filePath);
+ logEvent('tengu_ext_at_mentioned', {})
+ let atMentionedText: string
+ const relativePath = path.relative(getCwd(), atMentioned.filePath)
if (atMentioned.lineStart && atMentioned.lineEnd) {
- atMentionedText = atMentioned.lineStart === atMentioned.lineEnd ? `@${relativePath}#L${atMentioned.lineStart} ` : `@${relativePath}#L${atMentioned.lineStart}-${atMentioned.lineEnd} `;
+ atMentionedText =
+ atMentioned.lineStart === atMentioned.lineEnd
+ ? `@${relativePath}#L${atMentioned.lineStart} `
+ : `@${relativePath}#L${atMentioned.lineStart}-${atMentioned.lineEnd} `
} else {
- atMentionedText = `@${relativePath} `;
+ atMentionedText = `@${relativePath} `
}
- const cursorChar = input[cursorOffset - 1] ?? ' ';
+ const cursorChar = input[cursorOffset - 1] ?? ' '
if (!/\s/.test(cursorChar)) {
- atMentionedText = ` ${atMentionedText}`;
+ atMentionedText = ` ${atMentionedText}`
}
- insertTextAtCursor(atMentionedText);
- };
- useIdeAtMentioned(mcpClients, onIdeAtMentioned);
+ insertTextAtCursor(atMentionedText)
+ }
+ useIdeAtMentioned(mcpClients, onIdeAtMentioned)
// Handler for chat:undo - undo last edit
const handleUndo = useCallback(() => {
if (canUndo) {
- const previousState = undo();
+ const previousState = undo()
if (previousState) {
- trackAndSetInput(previousState.text);
- setCursorOffset(previousState.cursorOffset);
- setPastedContents(previousState.pastedContents);
+ trackAndSetInput(previousState.text)
+ setCursorOffset(previousState.cursorOffset)
+ setPastedContents(previousState.pastedContents)
}
}
- }, [canUndo, undo, trackAndSetInput, setPastedContents]);
+ }, [canUndo, undo, trackAndSetInput, setPastedContents])
// Handler for chat:newline - insert a newline at the cursor position
const handleNewline = useCallback(() => {
- pushToBuffer(input, cursorOffset, pastedContents);
- const newInput = input.slice(0, cursorOffset) + '\n' + input.slice(cursorOffset);
- trackAndSetInput(newInput);
- setCursorOffset(cursorOffset + 1);
- }, [input, cursorOffset, trackAndSetInput, setCursorOffset, pushToBuffer, pastedContents]);
+ pushToBuffer(input, cursorOffset, pastedContents)
+ const newInput =
+ input.slice(0, cursorOffset) + '\n' + input.slice(cursorOffset)
+ trackAndSetInput(newInput)
+ setCursorOffset(cursorOffset + 1)
+ }, [
+ input,
+ cursorOffset,
+ trackAndSetInput,
+ setCursorOffset,
+ pushToBuffer,
+ pastedContents,
+ ])
// Handler for chat:externalEditor - edit in $EDITOR
const handleExternalEditor = useCallback(async () => {
- logEvent('tengu_external_editor_used', {});
- setIsExternalEditorActive(true);
+ logEvent('tengu_external_editor_used', {})
+ setIsExternalEditorActive(true)
+
try {
// Pass pastedContents to expand collapsed text references
- const result = await editPromptInEditor(input, pastedContents);
+ const result = await editPromptInEditor(input, pastedContents)
+
if (result.error) {
addNotification({
key: 'external-editor-error',
text: result.error,
color: 'warning',
- priority: 'high'
- });
+ priority: 'high',
+ })
}
+
if (result.content !== null && result.content !== input) {
// Push current state to buffer before making changes
- pushToBuffer(input, cursorOffset, pastedContents);
- trackAndSetInput(result.content);
- setCursorOffset(result.content.length);
+ pushToBuffer(input, cursorOffset, pastedContents)
+
+ trackAndSetInput(result.content)
+ setCursorOffset(result.content.length)
}
} catch (err) {
if (err instanceof Error) {
- logError(err);
+ logError(err)
}
addNotification({
key: 'external-editor-error',
text: `External editor failed: ${errorMessage(err)}`,
color: 'warning',
- priority: 'high'
- });
+ priority: 'high',
+ })
} finally {
- setIsExternalEditorActive(false);
+ setIsExternalEditorActive(false)
}
- }, [input, cursorOffset, pastedContents, pushToBuffer, trackAndSetInput, addNotification]);
+ }, [
+ input,
+ cursorOffset,
+ pastedContents,
+ pushToBuffer,
+ trackAndSetInput,
+ addNotification,
+ ])
// Handler for chat:stash - stash/unstash prompt
const handleStash = useCallback(() => {
if (input.trim() === '' && stashedPrompt !== undefined) {
// Pop stash when input is empty
- trackAndSetInput(stashedPrompt.text);
- setCursorOffset(stashedPrompt.cursorOffset);
- setPastedContents(stashedPrompt.pastedContents);
- setStashedPrompt(undefined);
+ trackAndSetInput(stashedPrompt.text)
+ setCursorOffset(stashedPrompt.cursorOffset)
+ setPastedContents(stashedPrompt.pastedContents)
+ setStashedPrompt(undefined)
} else if (input.trim() !== '') {
// Push to stash (save text, cursor position, and pasted contents)
- setStashedPrompt({
- text: input,
- cursorOffset,
- pastedContents
- });
- trackAndSetInput('');
- setCursorOffset(0);
- setPastedContents({});
+ setStashedPrompt({ text: input, cursorOffset, pastedContents })
+ trackAndSetInput('')
+ setCursorOffset(0)
+ setPastedContents({})
// Track usage for /discover and stop showing hint
saveGlobalConfig(c => {
- if (c.hasUsedStash) return c;
- return {
- ...c,
- hasUsedStash: true
- };
- });
+ if (c.hasUsedStash) return c
+ return { ...c, hasUsedStash: true }
+ })
}
- }, [input, cursorOffset, stashedPrompt, trackAndSetInput, setStashedPrompt, pastedContents, setPastedContents]);
+ }, [
+ input,
+ cursorOffset,
+ stashedPrompt,
+ trackAndSetInput,
+ setStashedPrompt,
+ pastedContents,
+ setPastedContents,
+ ])
// Handler for chat:modelPicker - toggle model picker
const handleModelPicker = useCallback(() => {
- setShowModelPicker(prev => !prev);
+ setShowModelPicker(prev => !prev)
if (helpOpen) {
- setHelpOpen(false);
+ setHelpOpen(false)
}
- }, [helpOpen]);
+ }, [helpOpen])
// Handler for chat:fastMode - toggle fast mode picker
const handleFastModePicker = useCallback(() => {
- setShowFastModePicker(prev => !prev);
+ setShowFastModePicker(prev => !prev)
if (helpOpen) {
- setHelpOpen(false);
+ setHelpOpen(false)
}
- }, [helpOpen]);
+ }, [helpOpen])
// Handler for chat:thinkingToggle - toggle thinking mode
const handleThinkingToggle = useCallback(() => {
- setShowThinkingToggle(prev => !prev);
+ setShowThinkingToggle(prev => !prev)
if (helpOpen) {
- setHelpOpen(false);
+ setHelpOpen(false)
}
- }, [helpOpen]);
+ }, [helpOpen])
// Handler for chat:cycleMode - cycle through permission modes
const handleCycleMode = useCallback(() => {
@@ -1412,21 +1847,23 @@ function PromptInput({
if (isAgentSwarmsEnabled() && viewedTeammate && viewingAgentTaskId) {
const teammateContext: ToolPermissionContext = {
...toolPermissionContext,
- mode: viewedTeammate.permissionMode
- };
+ mode: viewedTeammate.permissionMode,
+ }
// Pass undefined for teamContext (unused but kept for API compatibility)
- const nextMode = getNextPermissionMode(teammateContext, undefined);
+ const nextMode = getNextPermissionMode(teammateContext, undefined)
+
logEvent('tengu_mode_cycle', {
- to: nextMode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
- const teammateTaskId = viewingAgentTaskId;
+ to: nextMode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
+
+ const teammateTaskId = viewingAgentTaskId
setAppState(prev => {
- const task = prev.tasks[teammateTaskId];
+ const task = prev.tasks[teammateTaskId]
if (!task || task.type !== 'in_process_teammate') {
- return prev;
+ return prev
}
if (task.permissionMode === nextMode) {
- return prev;
+ return prev
}
return {
...prev,
@@ -1434,34 +1871,42 @@ function PromptInput({
...prev.tasks,
[teammateTaskId]: {
...task,
- permissionMode: nextMode
- }
- }
- };
- });
+ permissionMode: nextMode,
+ },
+ },
+ }
+ })
+
if (helpOpen) {
- setHelpOpen(false);
+ setHelpOpen(false)
}
- return;
+ return
}
// Compute the next mode without triggering side effects first
- logForDebugging(`[auto-mode] handleCycleMode: currentMode=${toolPermissionContext.mode} isAutoModeAvailable=${toolPermissionContext.isAutoModeAvailable} showAutoModeOptIn=${showAutoModeOptIn} timeoutPending=${!!autoModeOptInTimeoutRef.current}`);
- const nextMode = getNextPermissionMode(toolPermissionContext, teamContext);
+ logForDebugging(
+ `[auto-mode] handleCycleMode: currentMode=${toolPermissionContext.mode} isAutoModeAvailable=${toolPermissionContext.isAutoModeAvailable} showAutoModeOptIn=${showAutoModeOptIn} timeoutPending=${!!autoModeOptInTimeoutRef.current}`,
+ )
+ const nextMode = getNextPermissionMode(toolPermissionContext, teamContext)
// Check if user is entering auto mode for the first time. Gated on the
// persistent settings flag (hasAutoModeOptIn) rather than the broader
// hasAutoModeOptInAnySource so that --enable-auto-mode users still see
// the warning dialog once — the CLI flag should grant carousel access,
// not bypass the safety text.
- let isEnteringAutoModeFirstTime = false;
+ let isEnteringAutoModeFirstTime = false
if (feature('TRANSCRIPT_CLASSIFIER')) {
- isEnteringAutoModeFirstTime = nextMode === 'auto' && toolPermissionContext.mode !== 'auto' && !hasAutoModeOptIn() && !viewingAgentTaskId; // Only show for primary agent, not subagents
+ isEnteringAutoModeFirstTime =
+ nextMode === 'auto' &&
+ toolPermissionContext.mode !== 'auto' &&
+ !hasAutoModeOptIn() &&
+ !viewingAgentTaskId // Only show for primary agent, not subagents
}
+
if (feature('TRANSCRIPT_CLASSIFIER')) {
if (isEnteringAutoModeFirstTime) {
// Store previous mode so we can revert if user declines
- setPreviousModeBeforeAuto(toolPermissionContext.mode);
+ setPreviousModeBeforeAuto(toolPermissionContext.mode)
// Only update the UI mode label — do NOT call transitionPermissionMode
// or cyclePermissionMode yet; we haven't confirmed with the user.
@@ -1469,26 +1914,32 @@ function PromptInput({
...prev,
toolPermissionContext: {
...prev.toolPermissionContext,
- mode: 'auto'
- }
- }));
+ mode: 'auto',
+ },
+ }))
setToolPermissionContext({
...toolPermissionContext,
- mode: 'auto'
- });
+ mode: 'auto',
+ })
// Show opt-in dialog after 400ms debounce
if (autoModeOptInTimeoutRef.current) {
- clearTimeout(autoModeOptInTimeoutRef.current);
+ clearTimeout(autoModeOptInTimeoutRef.current)
}
- autoModeOptInTimeoutRef.current = setTimeout((setShowAutoModeOptIn, autoModeOptInTimeoutRef) => {
- setShowAutoModeOptIn(true);
- autoModeOptInTimeoutRef.current = null;
- }, 400, setShowAutoModeOptIn, autoModeOptInTimeoutRef);
+ autoModeOptInTimeoutRef.current = setTimeout(
+ (setShowAutoModeOptIn, autoModeOptInTimeoutRef) => {
+ setShowAutoModeOptIn(true)
+ autoModeOptInTimeoutRef.current = null
+ },
+ 400,
+ setShowAutoModeOptIn,
+ autoModeOptInTimeoutRef,
+ )
+
if (helpOpen) {
- setHelpOpen(false);
+ setHelpOpen(false)
}
- return;
+ return
}
}
@@ -1500,14 +1951,14 @@ function PromptInput({
if (feature('TRANSCRIPT_CLASSIFIER')) {
if (showAutoModeOptIn || autoModeOptInTimeoutRef.current) {
if (showAutoModeOptIn) {
- logEvent('tengu_auto_mode_opt_in_dialog_decline', {});
+ logEvent('tengu_auto_mode_opt_in_dialog_decline', {})
}
- setShowAutoModeOptIn(false);
+ setShowAutoModeOptIn(false)
if (autoModeOptInTimeoutRef.current) {
- clearTimeout(autoModeOptInTimeoutRef.current);
- autoModeOptInTimeoutRef.current = null;
+ clearTimeout(autoModeOptInTimeoutRef.current)
+ autoModeOptInTimeoutRef.current = null
}
- setPreviousModeBeforeAuto(null);
+ setPreviousModeBeforeAuto(null)
// Fall through — mode is 'auto', cyclePermissionMode below goes to 'default'.
}
}
@@ -1515,19 +1966,21 @@ function PromptInput({
// Now that we know this is NOT the first-time auto mode path,
// call cyclePermissionMode to apply side effects (e.g. strip
// dangerous permissions, activate classifier)
- const {
- context: preparedContext
- } = cyclePermissionMode(toolPermissionContext, teamContext);
+ const { context: preparedContext } = cyclePermissionMode(
+ toolPermissionContext,
+ teamContext,
+ )
+
logEvent('tengu_mode_cycle', {
- to: nextMode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
+ to: nextMode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
// Track when user enters plan mode
if (nextMode === 'plan') {
saveGlobalConfig(current => ({
...current,
- lastPlanModeUse: Date.now()
- }));
+ lastPlanModeUse: Date.now(),
+ }))
}
// Set the mode via setAppState directly because setToolPermissionContext
@@ -1538,101 +1991,134 @@ function PromptInput({
...prev,
toolPermissionContext: {
...preparedContext,
- mode: nextMode
- }
- }));
+ mode: nextMode,
+ },
+ }))
setToolPermissionContext({
...preparedContext,
- mode: nextMode
- });
+ mode: nextMode,
+ })
// If this is a teammate, update config.json so team lead sees the change
- syncTeammateMode(nextMode, teamContext?.teamName);
+ syncTeammateMode(nextMode, teamContext?.teamName)
// Close help tips if they're open when mode is cycled
if (helpOpen) {
- setHelpOpen(false);
+ setHelpOpen(false)
}
- }, [toolPermissionContext, teamContext, viewingAgentTaskId, viewedTeammate, setAppState, setToolPermissionContext, helpOpen, showAutoModeOptIn]);
+ }, [
+ toolPermissionContext,
+ teamContext,
+ viewingAgentTaskId,
+ viewedTeammate,
+ setAppState,
+ setToolPermissionContext,
+ helpOpen,
+ showAutoModeOptIn,
+ ])
// Handler for auto mode opt-in dialog acceptance
const handleAutoModeOptInAccept = useCallback(() => {
if (feature('TRANSCRIPT_CLASSIFIER')) {
- setShowAutoModeOptIn(false);
- setPreviousModeBeforeAuto(null);
+ setShowAutoModeOptIn(false)
+ setPreviousModeBeforeAuto(null)
// Now that the user accepted, apply the full transition: activate the
// auto mode backend (classifier, beta headers) and strip dangerous
// permissions (e.g. Bash(*) always-allow rules).
- const strippedContext = transitionPermissionMode(previousModeBeforeAuto ?? toolPermissionContext.mode, 'auto', toolPermissionContext);
+ const strippedContext = transitionPermissionMode(
+ previousModeBeforeAuto ?? toolPermissionContext.mode,
+ 'auto',
+ toolPermissionContext,
+ )
setAppState(prev => ({
...prev,
toolPermissionContext: {
...strippedContext,
- mode: 'auto'
- }
- }));
+ mode: 'auto',
+ },
+ }))
setToolPermissionContext({
...strippedContext,
- mode: 'auto'
- });
+ mode: 'auto',
+ })
// Close help tips if they're open when auto mode is enabled
if (helpOpen) {
- setHelpOpen(false);
+ setHelpOpen(false)
}
}
- }, [helpOpen, setHelpOpen, previousModeBeforeAuto, toolPermissionContext, setAppState, setToolPermissionContext]);
+ }, [
+ helpOpen,
+ setHelpOpen,
+ previousModeBeforeAuto,
+ toolPermissionContext,
+ setAppState,
+ setToolPermissionContext,
+ ])
// Handler for auto mode opt-in dialog decline
const handleAutoModeOptInDecline = useCallback(() => {
if (feature('TRANSCRIPT_CLASSIFIER')) {
- logForDebugging(`[auto-mode] handleAutoModeOptInDecline: reverting to ${previousModeBeforeAuto}, setting isAutoModeAvailable=false`);
- setShowAutoModeOptIn(false);
+ logForDebugging(
+ `[auto-mode] handleAutoModeOptInDecline: reverting to ${previousModeBeforeAuto}, setting isAutoModeAvailable=false`,
+ )
+ setShowAutoModeOptIn(false)
if (autoModeOptInTimeoutRef.current) {
- clearTimeout(autoModeOptInTimeoutRef.current);
- autoModeOptInTimeoutRef.current = null;
+ clearTimeout(autoModeOptInTimeoutRef.current)
+ autoModeOptInTimeoutRef.current = null
}
// Revert to previous mode and remove auto from the carousel
// for the rest of this session
if (previousModeBeforeAuto) {
- setAutoModeActive(false);
+ setAutoModeActive(false)
setAppState(prev => ({
...prev,
toolPermissionContext: {
...prev.toolPermissionContext,
mode: previousModeBeforeAuto,
- isAutoModeAvailable: false
- }
- }));
+ isAutoModeAvailable: false,
+ },
+ }))
setToolPermissionContext({
...toolPermissionContext,
mode: previousModeBeforeAuto,
- isAutoModeAvailable: false
- });
- setPreviousModeBeforeAuto(null);
+ isAutoModeAvailable: false,
+ })
+ setPreviousModeBeforeAuto(null)
}
}
- }, [previousModeBeforeAuto, toolPermissionContext, setAppState, setToolPermissionContext]);
+ }, [
+ previousModeBeforeAuto,
+ toolPermissionContext,
+ setAppState,
+ setToolPermissionContext,
+ ])
// Handler for chat:imagePaste - paste image from clipboard
const handleImagePaste = useCallback(() => {
void getImageFromClipboard().then(imageData => {
if (imageData) {
- onImagePaste(imageData.base64, imageData.mediaType);
+ onImagePaste(imageData.base64, imageData.mediaType)
} else {
- const shortcutDisplay = getShortcutDisplay('chat:imagePaste', 'Chat', 'ctrl+v');
- const message = env.isSSH() ? "No image found in clipboard. You're SSH'd; try scp?" : `No image found in clipboard. Use ${shortcutDisplay} to paste images.`;
+ const shortcutDisplay = getShortcutDisplay(
+ 'chat:imagePaste',
+ 'Chat',
+ 'ctrl+v',
+ )
+ const message = env.isSSH()
+ ? "No image found in clipboard. You're SSH'd; try scp?"
+ : `No image found in clipboard. Use ${shortcutDisplay} to paste images.`
addNotification({
key: 'no-image-in-clipboard',
text: message,
priority: 'immediate',
- timeoutMs: 1000
- });
+ timeoutMs: 1000,
+ })
}
- });
- }, [addNotification, onImagePaste]);
+ })
+ }, [addNotification, onImagePaste])
// Register chat:submit handler directly in the handler registry (not via
// useKeybindings) so that only the ChordInterceptor can invoke it for chord
@@ -1640,250 +2126,309 @@ function PromptInput({
// handled by TextInput directly (via onSubmit prop) and useTypeahead (for
// autocomplete acceptance). Using useKeybindings would cause
// stopImmediatePropagation on Enter, blocking autocomplete from seeing the key.
- const keybindingContext = useOptionalKeybindingContext();
+ const keybindingContext = useOptionalKeybindingContext()
useEffect(() => {
- if (!keybindingContext || isModalOverlayActive) return;
+ if (!keybindingContext || isModalOverlayActive) return
return keybindingContext.registerHandler({
action: 'chat:submit',
context: 'Chat',
handler: () => {
- void onSubmit(input);
- }
- });
- }, [keybindingContext, isModalOverlayActive, onSubmit, input]);
+ void onSubmit(input)
+ },
+ })
+ }, [keybindingContext, isModalOverlayActive, onSubmit, input])
// Chat context keybindings for editing shortcuts
// Note: history:previous/history:next are NOT handled here. They are passed as
// onHistoryUp/onHistoryDown props to TextInput, so that useTextInput's
// upOrHistoryUp/downOrHistoryDown can try cursor movement first and only
// fall through to history when the cursor can't move further.
- const chatHandlers = useMemo(() => ({
- 'chat:undo': handleUndo,
- 'chat:newline': handleNewline,
- 'chat:externalEditor': handleExternalEditor,
- 'chat:stash': handleStash,
- 'chat:modelPicker': handleModelPicker,
- 'chat:thinkingToggle': handleThinkingToggle,
- 'chat:cycleMode': handleCycleMode,
- 'chat:imagePaste': handleImagePaste
- }), [handleUndo, handleNewline, handleExternalEditor, handleStash, handleModelPicker, handleThinkingToggle, handleCycleMode, handleImagePaste]);
+ const chatHandlers = useMemo(
+ () => ({
+ 'chat:undo': handleUndo,
+ 'chat:newline': handleNewline,
+ 'chat:externalEditor': handleExternalEditor,
+ 'chat:stash': handleStash,
+ 'chat:modelPicker': handleModelPicker,
+ 'chat:thinkingToggle': handleThinkingToggle,
+ 'chat:cycleMode': handleCycleMode,
+ 'chat:imagePaste': handleImagePaste,
+ }),
+ [
+ handleUndo,
+ handleNewline,
+ handleExternalEditor,
+ handleStash,
+ handleModelPicker,
+ handleThinkingToggle,
+ handleCycleMode,
+ handleImagePaste,
+ ],
+ )
+
useKeybindings(chatHandlers, {
context: 'Chat',
- isActive: !isModalOverlayActive
- });
+ isActive: !isModalOverlayActive,
+ })
// Shift+↑ enters message-actions cursor. Separate isActive so ctrl+r search
// doesn't leave stale isSearchingHistory on cursor-exit remount.
useKeybinding('chat:messageActions', () => onMessageActionsEnter?.(), {
context: 'Chat',
- isActive: !isModalOverlayActive && !isSearchingHistory
- });
+ isActive: !isModalOverlayActive && !isSearchingHistory,
+ })
// Fast mode keybinding is only active when fast mode is enabled and available
useKeybinding('chat:fastMode', handleFastModePicker, {
context: 'Chat',
- isActive: !isModalOverlayActive && isFastModeEnabled() && isFastModeAvailable()
- });
+ isActive:
+ !isModalOverlayActive && isFastModeEnabled() && isFastModeAvailable(),
+ })
// Handle help:dismiss keybinding (ESC closes help menu)
// This is registered separately from Chat context so it has priority over
// CancelRequestHandler when help menu is open
- useKeybinding('help:dismiss', () => {
- setHelpOpen(false);
- }, {
- context: 'Help',
- isActive: helpOpen
- });
+ useKeybinding(
+ 'help:dismiss',
+ () => {
+ setHelpOpen(false)
+ },
+ { context: 'Help', isActive: helpOpen },
+ )
// Quick Open / Global Search. Hook calls are unconditional (Rules of Hooks);
// the handler body is feature()-gated so the setState calls and component
// references get tree-shaken in external builds.
- const quickSearchActive = feature('QUICK_SEARCH') ? !isModalOverlayActive : false;
- useKeybinding('app:quickOpen', () => {
- if (feature('QUICK_SEARCH')) {
- setShowQuickOpen(true);
- setHelpOpen(false);
- }
- }, {
- context: 'Global',
- isActive: quickSearchActive
- });
- useKeybinding('app:globalSearch', () => {
- if (feature('QUICK_SEARCH')) {
- setShowGlobalSearch(true);
- setHelpOpen(false);
- }
- }, {
- context: 'Global',
- isActive: quickSearchActive
- });
- useKeybinding('history:search', () => {
- if (feature('HISTORY_PICKER')) {
- setShowHistoryPicker(true);
- setHelpOpen(false);
- }
- }, {
- context: 'Global',
- isActive: feature('HISTORY_PICKER') ? !isModalOverlayActive : false
- });
+ const quickSearchActive = feature('QUICK_SEARCH')
+ ? !isModalOverlayActive
+ : false
+ useKeybinding(
+ 'app:quickOpen',
+ () => {
+ if (feature('QUICK_SEARCH')) {
+ setShowQuickOpen(true)
+ setHelpOpen(false)
+ }
+ },
+ { context: 'Global', isActive: quickSearchActive },
+ )
+ useKeybinding(
+ 'app:globalSearch',
+ () => {
+ if (feature('QUICK_SEARCH')) {
+ setShowGlobalSearch(true)
+ setHelpOpen(false)
+ }
+ },
+ { context: 'Global', isActive: quickSearchActive },
+ )
+
+ useKeybinding(
+ 'history:search',
+ () => {
+ if (feature('HISTORY_PICKER')) {
+ setShowHistoryPicker(true)
+ setHelpOpen(false)
+ }
+ },
+ {
+ context: 'Global',
+ isActive: feature('HISTORY_PICKER') ? !isModalOverlayActive : false,
+ },
+ )
// Handle Ctrl+C to abort speculation when idle (not loading)
// CancelRequestHandler only handles Ctrl+C during active tasks
- useKeybinding('app:interrupt', () => {
- abortSpeculation(setAppState);
- }, {
- context: 'Global',
- isActive: !isLoading && speculation.status === 'active'
- });
+ useKeybinding(
+ 'app:interrupt',
+ () => {
+ abortSpeculation(setAppState)
+ },
+ {
+ context: 'Global',
+ isActive: !isLoading && speculation.status === 'active',
+ },
+ )
// Footer indicator navigation keybindings. ↑/↓ live here (not in
// handleHistoryUp/Down) because TextInput focus=false when a pill is
// selected — its useInput is inactive, so this is the only path.
- useKeybindings({
- 'footer:up': () => {
- // ↑ scrolls within the coordinator task list before leaving the pill
- if (tasksSelected && (process.env.USER_TYPE) === 'ant' && coordinatorTaskCount > 0 && coordinatorTaskIndex > minCoordinatorIndex) {
- setCoordinatorTaskIndex(prev => prev - 1);
- return;
- }
- navigateFooter(-1, true);
- },
- 'footer:down': () => {
- // ↓ scrolls within the coordinator task list, never leaves the pill
- if (tasksSelected && (process.env.USER_TYPE) === 'ant' && coordinatorTaskCount > 0) {
- if (coordinatorTaskIndex < coordinatorTaskCount - 1) {
- setCoordinatorTaskIndex(prev => prev + 1);
+ useKeybindings(
+ {
+ 'footer:up': () => {
+ // ↑ scrolls within the coordinator task list before leaving the pill
+ if (
+ tasksSelected &&
+ process.env.USER_TYPE === 'ant' &&
+ coordinatorTaskCount > 0 &&
+ coordinatorTaskIndex > minCoordinatorIndex
+ ) {
+ setCoordinatorTaskIndex(prev => prev - 1)
+ return
}
- return;
- }
- if (tasksSelected && !isTeammateMode) {
- setShowBashesDialog(true);
- selectFooterItem(null);
- return;
- }
- navigateFooter(1);
- },
- 'footer:next': () => {
- // Teammate mode: ←/→ cycles within the team member list
- if (tasksSelected && isTeammateMode) {
- const totalAgents = 1 + inProcessTeammates.length;
- setTeammateFooterIndex(prev => (prev + 1) % totalAgents);
- return;
- }
- navigateFooter(1);
- },
- 'footer:previous': () => {
- if (tasksSelected && isTeammateMode) {
- const totalAgents = 1 + inProcessTeammates.length;
- setTeammateFooterIndex(prev => (prev - 1 + totalAgents) % totalAgents);
- return;
- }
- navigateFooter(-1);
- },
- 'footer:openSelected': () => {
- if (viewSelectionMode === 'selecting-agent') {
- return;
- }
- switch (footerItemSelected) {
- case 'companion':
- if (feature('BUDDY')) {
- selectFooterItem(null);
- void onSubmit('/buddy');
+ navigateFooter(-1, true)
+ },
+ 'footer:down': () => {
+ // ↓ scrolls within the coordinator task list, never leaves the pill
+ if (
+ tasksSelected &&
+ process.env.USER_TYPE === 'ant' &&
+ coordinatorTaskCount > 0
+ ) {
+ if (coordinatorTaskIndex < coordinatorTaskCount - 1) {
+ setCoordinatorTaskIndex(prev => prev + 1)
}
- break;
- case 'tasks':
- if (isTeammateMode) {
- // Enter switches to the selected agent's view
- if (teammateFooterIndex === 0) {
- exitTeammateView(setAppState);
- } else {
- const teammate = inProcessTeammates[teammateFooterIndex - 1];
- if (teammate) enterTeammateView(teammate.id, setAppState);
+ return
+ }
+ if (tasksSelected && !isTeammateMode) {
+ setShowBashesDialog(true)
+ selectFooterItem(null)
+ return
+ }
+ navigateFooter(1)
+ },
+ 'footer:next': () => {
+ // Teammate mode: ←/→ cycles within the team member list
+ if (tasksSelected && isTeammateMode) {
+ const totalAgents = 1 + inProcessTeammates.length
+ setTeammateFooterIndex(prev => (prev + 1) % totalAgents)
+ return
+ }
+ navigateFooter(1)
+ },
+ 'footer:previous': () => {
+ if (tasksSelected && isTeammateMode) {
+ const totalAgents = 1 + inProcessTeammates.length
+ setTeammateFooterIndex(prev => (prev - 1 + totalAgents) % totalAgents)
+ return
+ }
+ navigateFooter(-1)
+ },
+ 'footer:openSelected': () => {
+ if (viewSelectionMode === 'selecting-agent') {
+ return
+ }
+ switch (footerItemSelected) {
+ case 'companion':
+ if (feature('BUDDY')) {
+ selectFooterItem(null)
+ void onSubmit('/buddy')
}
- } else if (coordinatorTaskIndex === 0 && coordinatorTaskCount > 0) {
- exitTeammateView(setAppState);
- } else {
- const selectedTaskId = getVisibleAgentTasks(tasks)[coordinatorTaskIndex - 1]?.id;
- if (selectedTaskId) {
- enterTeammateView(selectedTaskId, setAppState);
+ break
+ case 'tasks':
+ if (isTeammateMode) {
+ // Enter switches to the selected agent's view
+ if (teammateFooterIndex === 0) {
+ exitTeammateView(setAppState)
+ } else {
+ const teammate = inProcessTeammates[teammateFooterIndex - 1]
+ if (teammate) enterTeammateView(teammate.id, setAppState)
+ }
+ } else if (coordinatorTaskIndex === 0 && coordinatorTaskCount > 0) {
+ exitTeammateView(setAppState)
} else {
- setShowBashesDialog(true);
- selectFooterItem(null);
+ const selectedTaskId =
+ getVisibleAgentTasks(tasks)[coordinatorTaskIndex - 1]?.id
+ if (selectedTaskId) {
+ enterTeammateView(selectedTaskId, setAppState)
+ } else {
+ setShowBashesDialog(true)
+ selectFooterItem(null)
+ }
}
- }
- break;
- case 'tmux':
- if ((process.env.USER_TYPE) === 'ant') {
- setAppState(prev => prev.tungstenPanelAutoHidden ? {
- ...prev,
- tungstenPanelAutoHidden: false
- } : {
- ...prev,
- tungstenPanelVisible: !(prev.tungstenPanelVisible ?? true)
- });
- }
- break;
- case 'bagel':
- break;
- case 'teams':
- setShowTeamsDialog(true);
- selectFooterItem(null);
- break;
- case 'bridge':
- setShowBridgeDialog(true);
- selectFooterItem(null);
- break;
- }
- },
- 'footer:clearSelection': () => {
- selectFooterItem(null);
- },
- 'footer:close': () => {
- if (tasksSelected && coordinatorTaskIndex >= 1) {
- const task = getVisibleAgentTasks(tasks)[coordinatorTaskIndex - 1];
- if (!task) return false;
- // When the selected row IS the viewed agent, 'x' types into the
- // steering input. Any other row — dismiss it.
- if (viewSelectionMode === 'viewing-agent' && task.id === viewingAgentTaskId) {
- onChange(input.slice(0, cursorOffset) + 'x' + input.slice(cursorOffset));
- setCursorOffset(cursorOffset + 1);
- return;
+ break
+ case 'tmux':
+ if (process.env.USER_TYPE === 'ant') {
+ setAppState(prev =>
+ prev.tungstenPanelAutoHidden
+ ? { ...prev, tungstenPanelAutoHidden: false }
+ : {
+ ...prev,
+ tungstenPanelVisible: !(
+ prev.tungstenPanelVisible ?? true
+ ),
+ },
+ )
+ }
+ break
+ case 'bagel':
+ break
+ case 'teams':
+ setShowTeamsDialog(true)
+ selectFooterItem(null)
+ break
+ case 'bridge':
+ setShowBridgeDialog(true)
+ selectFooterItem(null)
+ break
}
- stopOrDismissAgent(task.id, setAppState);
- if (task.status !== 'running') {
- setCoordinatorTaskIndex(i => Math.max(minCoordinatorIndex, i - 1));
+ },
+ 'footer:clearSelection': () => {
+ selectFooterItem(null)
+ },
+ 'footer:close': () => {
+ if (tasksSelected && coordinatorTaskIndex >= 1) {
+ const task = getVisibleAgentTasks(tasks)[coordinatorTaskIndex - 1]
+ if (!task) return false
+ // When the selected row IS the viewed agent, 'x' types into the
+ // steering input. Any other row — dismiss it.
+ if (
+ viewSelectionMode === 'viewing-agent' &&
+ task.id === viewingAgentTaskId
+ ) {
+ onChange(
+ input.slice(0, cursorOffset) + 'x' + input.slice(cursorOffset),
+ )
+ setCursorOffset(cursorOffset + 1)
+ return
+ }
+ stopOrDismissAgent(task.id, setAppState)
+ if (task.status !== 'running') {
+ setCoordinatorTaskIndex(i => Math.max(minCoordinatorIndex, i - 1))
+ }
+ return
}
- return;
- }
- // Not handled — let 'x' fall through to type-to-exit
- return false;
- }
- }, {
- context: 'Footer',
- isActive: !!footerItemSelected && !isModalOverlayActive
- });
+ // Not handled — let 'x' fall through to type-to-exit
+ return false
+ },
+ },
+ {
+ context: 'Footer',
+ isActive: !!footerItemSelected && !isModalOverlayActive,
+ },
+ )
+
useInput((char, key) => {
// Skip all input handling when a full-screen dialog is open. These dialogs
// render via early return, but hooks run unconditionally — so without this
// guard, Escape inside a dialog leaks to the double-press message-selector.
- if (showTeamsDialog || showQuickOpen || showGlobalSearch || showHistoryPicker) {
- return;
+ if (
+ showTeamsDialog ||
+ showQuickOpen ||
+ showGlobalSearch ||
+ showHistoryPicker
+ ) {
+ return
}
// Detect failed Alt shortcuts on macOS (Option key produces special characters)
if (getPlatform() === 'macos' && isMacosOptionChar(char)) {
- const shortcut = MACOS_OPTION_SPECIAL_CHARS[char];
- const terminalName = getNativeCSIuTerminalDisplayName();
- const jsx = terminalName ?
+ const shortcut = MACOS_OPTION_SPECIAL_CHARS[char]
+ const terminalName = getNativeCSIuTerminalDisplayName()
+ const jsx = terminalName ? (
+
To enable {shortcut}, set Option as Meta in{' '}
{terminalName} preferences (⌘,)
- : To enable {shortcut}, run /terminal-setup;
+
+ ) : (
+ To enable {shortcut}, run /terminal-setup
+ )
addNotification({
key: 'option-meta-hint',
jsx,
priority: 'immediate',
- timeoutMs: 5000
- });
+ timeoutMs: 5000,
+ })
// Don't return - let the character be typed so user sees the issue
}
@@ -1895,21 +2440,31 @@ function PromptInput({
// the input and type the char. Nav keys are captured by useKeybindings
// above, so anything reaching here is genuinely not a footer action.
// onChange clears footerSelection, so no explicit deselect.
- if (footerItemSelected && char && !key.ctrl && !key.meta && !key.escape && !key.return) {
- onChange(input.slice(0, cursorOffset) + char + input.slice(cursorOffset));
- setCursorOffset(cursorOffset + char.length);
- return;
+ if (
+ footerItemSelected &&
+ char &&
+ !key.ctrl &&
+ !key.meta &&
+ !key.escape &&
+ !key.return
+ ) {
+ onChange(input.slice(0, cursorOffset) + char + input.slice(cursorOffset))
+ setCursorOffset(cursorOffset + char.length)
+ return
}
// Exit special modes when backspace/escape/delete/ctrl+u is pressed at cursor position 0
- if (cursorOffset === 0 && (key.escape || key.backspace || key.delete || key.ctrl && char === 'u')) {
- onModeChange('prompt');
- setHelpOpen(false);
+ if (
+ cursorOffset === 0 &&
+ (key.escape || key.backspace || key.delete || (key.ctrl && char === 'u'))
+ ) {
+ onModeChange('prompt')
+ setHelpOpen(false)
}
// Exit help mode when backspace is pressed and input is empty
if (helpOpen && input === '' && (key.backspace || key.delete)) {
- setHelpOpen(false);
+ setHelpOpen(false)
}
// esc is a little overloaded:
@@ -1922,73 +2477,83 @@ function PromptInput({
if (key.escape) {
// Abort active speculation
if (speculation.status === 'active') {
- abortSpeculation(setAppState);
- return;
+ abortSpeculation(setAppState)
+ return
}
// Dismiss side question response if visible
if (isSideQuestionVisible && onDismissSideQuestion) {
- onDismissSideQuestion();
- return;
+ onDismissSideQuestion()
+ return
}
// Close help menu if open
if (helpOpen) {
- setHelpOpen(false);
- return;
+ setHelpOpen(false)
+ return
}
// Footer selection clearing is now handled via Footer context keybindings
// (footer:clearSelection action bound to escape)
// If a footer item is selected, let the Footer keybinding handle it
if (footerItemSelected) {
- return;
+ return
}
// If there's an editable queued command, move it to the input for editing when ESC is pressed
- const hasEditableCommand = queuedCommands.some(isQueuedCommandEditable);
+ const hasEditableCommand = queuedCommands.some(isQueuedCommandEditable)
if (hasEditableCommand) {
- void popAllCommandsFromQueue();
- return;
+ void popAllCommandsFromQueue()
+ return
}
+
if (messages.length > 0 && !input && !isLoading) {
- doublePressEscFromEmpty();
+ doublePressEscFromEmpty()
}
}
+
if (key.return && helpOpen) {
- setHelpOpen(false);
+ setHelpOpen(false)
}
- });
- const swarmBanner = useSwarmBanner();
- const fastModeCooldown = isFastModeEnabled() ? isFastModeCooldown() : false;
- const showFastIcon = isFastModeEnabled() ? isFastMode && (isFastModeAvailable() || fastModeCooldown) : false;
- const showFastIconHint = useShowFastIconHint(showFastIcon ?? false);
+ })
+
+ const swarmBanner = useSwarmBanner()
+
+ const fastModeCooldown = isFastModeEnabled() ? isFastModeCooldown() : false
+ const showFastIcon = isFastModeEnabled()
+ ? isFastMode && (isFastModeAvailable() || fastModeCooldown)
+ : false
+
+ const showFastIconHint = useShowFastIconHint(showFastIcon ?? false)
// Show effort notification on startup and when effort changes.
// Suppressed in brief/assistant mode — the value reflects the local
// client's effort, not the connected agent's.
- const effortNotificationText = briefOwnsGap ? undefined : getEffortNotificationText(effortValue, mainLoopModel);
+ const effortNotificationText = briefOwnsGap
+ ? undefined
+ : getEffortNotificationText(effortValue, mainLoopModel)
useEffect(() => {
if (!effortNotificationText) {
- removeNotification('effort-level');
- return;
+ removeNotification('effort-level')
+ return
}
addNotification({
key: 'effort-level',
text: effortNotificationText,
priority: 'high',
- timeoutMs: 12_000
- });
- }, [effortNotificationText, addNotification, removeNotification]);
- useBuddyNotification();
- const companionSpeaking = feature('BUDDY') ?
- // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- useAppState(s => s.companionReaction !== undefined) : false;
- const {
- columns,
- rows
- } = useTerminalSize();
- const textInputColumns = columns - 3 - companionReservedColumns(columns, companionSpeaking);
+ timeoutMs: 12_000,
+ })
+ }, [effortNotificationText, addNotification, removeNotification])
+
+ useBuddyNotification()
+
+ const companionSpeaking = feature('BUDDY')
+ ? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+ useAppState(s => s.companionReaction !== undefined)
+ : false
+ const { columns, rows } = useTerminalSize()
+ const textInputColumns =
+ columns - 3 - companionReservedColumns(columns, companionSpeaking)
// POC: click-to-position-cursor. Mouse tracking is only enabled inside
// , so this is dormant in the normal main-screen REPL.
@@ -1996,184 +2561,324 @@ function PromptInput({
// tightly wraps the text input so they map directly to (column, line)
// in the Cursor wrap model. MeasuredText.getOffsetFromPosition handles
// wide chars, wrapped lines, and clamps past-end clicks to line end.
- const maxVisibleLines = isFullscreenEnvEnabled() ? Math.max(MIN_INPUT_VIEWPORT_LINES, Math.floor(rows / 2) - PROMPT_FOOTER_LINES) : undefined;
- const handleInputClick = useCallback((e: ClickEvent) => {
- // During history search the displayed text is historyMatch, not
- // input, and showCursor is false anyway — skip rather than
- // compute an offset against the wrong string.
- if (!input || isSearchingHistory) return;
- const c = Cursor.fromText(input, textInputColumns, cursorOffset);
- const viewportStart = c.getViewportStartLine(maxVisibleLines);
- const offset = c.measuredText.getOffsetFromPosition({
- line: e.localRow + viewportStart,
- column: e.localCol
- });
- setCursorOffset(offset);
- }, [input, textInputColumns, isSearchingHistory, cursorOffset, maxVisibleLines]);
- const handleOpenTasksDialog = useCallback((taskId?: string) => setShowBashesDialog(taskId ?? true), [setShowBashesDialog]);
- const placeholder = showPromptSuggestion && promptSuggestion ? promptSuggestion : defaultPlaceholder;
+ const maxVisibleLines = isFullscreenEnvEnabled()
+ ? Math.max(
+ MIN_INPUT_VIEWPORT_LINES,
+ Math.floor(rows / 2) - PROMPT_FOOTER_LINES,
+ )
+ : undefined
+
+ const handleInputClick = useCallback(
+ (e: ClickEvent) => {
+ // During history search the displayed text is historyMatch, not
+ // input, and showCursor is false anyway — skip rather than
+ // compute an offset against the wrong string.
+ if (!input || isSearchingHistory) return
+ const c = Cursor.fromText(input, textInputColumns, cursorOffset)
+ const viewportStart = c.getViewportStartLine(maxVisibleLines)
+ const offset = c.measuredText.getOffsetFromPosition({
+ line: e.localRow + viewportStart,
+ column: e.localCol,
+ })
+ setCursorOffset(offset)
+ },
+ [
+ input,
+ textInputColumns,
+ isSearchingHistory,
+ cursorOffset,
+ maxVisibleLines,
+ ],
+ )
+
+ const handleOpenTasksDialog = useCallback(
+ (taskId?: string) => setShowBashesDialog(taskId ?? true),
+ [setShowBashesDialog],
+ )
+
+ const placeholder =
+ showPromptSuggestion && promptSuggestion
+ ? promptSuggestion
+ : defaultPlaceholder
// Calculate if input has multiple lines
- const isInputWrapped = useMemo(() => input.includes('\n'), [input]);
+ const isInputWrapped = useMemo(() => input.includes('\n'), [input])
// Memoized callbacks for model picker to prevent re-renders when unrelated
// state (like notifications) changes. This prevents the inline model picker
// from visually "jumping" when notifications arrive.
- const handleModelSelect = useCallback((model: string | null, _effort: EffortLevel | undefined) => {
- let wasFastModeDisabled = false;
- setAppState(prev => {
- wasFastModeDisabled = isFastModeEnabled() && !isFastModeSupportedByModel(model) && !!prev.fastMode;
- return {
- ...prev,
- mainLoopModel: model,
- mainLoopModelForSession: null,
- // Turn off fast mode if switching to a model that doesn't support it
- ...(wasFastModeDisabled && {
- fastMode: false
- })
- };
- });
- setShowModelPicker(false);
- const effectiveFastMode = (isFastMode ?? false) && !wasFastModeDisabled;
- let message = `Model set to ${modelDisplayString(model)}`;
- if (isBilledAsExtraUsage(model, effectiveFastMode, isOpus1mMergeEnabled())) {
- message += ' · Billed as extra usage';
- }
- if (wasFastModeDisabled) {
- message += ' · Fast mode OFF';
- }
- addNotification({
- key: 'model-switched',
- jsx: {message},
- priority: 'immediate',
- timeoutMs: 3000
- });
- logEvent('tengu_model_picker_hotkey', {
- model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
- });
- }, [setAppState, addNotification, isFastMode]);
+ const handleModelSelect = useCallback(
+ (model: string | null, _effort: EffortLevel | undefined) => {
+ let wasFastModeDisabled = false
+ setAppState(prev => {
+ wasFastModeDisabled =
+ isFastModeEnabled() &&
+ !isFastModeSupportedByModel(model) &&
+ !!prev.fastMode
+ return {
+ ...prev,
+ mainLoopModel: model,
+ mainLoopModelForSession: null,
+ // Turn off fast mode if switching to a model that doesn't support it
+ ...(wasFastModeDisabled && { fastMode: false }),
+ }
+ })
+ setShowModelPicker(false)
+ const effectiveFastMode = (isFastMode ?? false) && !wasFastModeDisabled
+ let message = `Model set to ${modelDisplayString(model)}`
+ if (
+ isBilledAsExtraUsage(model, effectiveFastMode, isOpus1mMergeEnabled())
+ ) {
+ message += ' · Billed as extra usage'
+ }
+ if (wasFastModeDisabled) {
+ message += ' · Fast mode OFF'
+ }
+ addNotification({
+ key: 'model-switched',
+ jsx: {message},
+ priority: 'immediate',
+ timeoutMs: 3000,
+ })
+ logEvent('tengu_model_picker_hotkey', {
+ model:
+ model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ })
+ },
+ [setAppState, addNotification, isFastMode],
+ )
+
const handleModelCancel = useCallback(() => {
- setShowModelPicker(false);
- }, []);
+ setShowModelPicker(false)
+ }, [])
// Memoize the model picker element to prevent unnecessary re-renders
// when AppState changes for unrelated reasons (e.g., notifications arriving)
const modelPickerElement = useMemo(() => {
- if (!showModelPicker) return null;
- return
-
- ;
- }, [showModelPicker, mainLoopModel_, mainLoopModelForSession, handleModelSelect, handleModelCancel]);
- const handleFastModeSelect = useCallback((result?: string) => {
- setShowFastModePicker(false);
- if (result) {
- addNotification({
- key: 'fast-mode-toggled',
- jsx: {result},
- priority: 'immediate',
- timeoutMs: 3000
- });
- }
- }, [addNotification]);
+ if (!showModelPicker) return null
+ return (
+
+
+
+ )
+ }, [
+ showModelPicker,
+ mainLoopModel_,
+ mainLoopModelForSession,
+ handleModelSelect,
+ handleModelCancel,
+ ])
+
+ const handleFastModeSelect = useCallback(
+ (result?: string) => {
+ setShowFastModePicker(false)
+ if (result) {
+ addNotification({
+ key: 'fast-mode-toggled',
+ jsx: {result},
+ priority: 'immediate',
+ timeoutMs: 3000,
+ })
+ }
+ },
+ [addNotification],
+ )
// Memoize the fast mode picker element
const fastModePickerElement = useMemo(() => {
- if (!showFastModePicker) return null;
- return
-
- ;
- }, [showFastModePicker, handleFastModeSelect]);
+ if (!showFastModePicker) return null
+ return (
+
+
+
+ )
+ }, [showFastModePicker, handleFastModeSelect])
// Memoized callbacks for thinking toggle
- const handleThinkingSelect = useCallback((enabled: boolean) => {
- setAppState(prev => ({
- ...prev,
- thinkingEnabled: enabled
- }));
- setShowThinkingToggle(false);
- logEvent('tengu_thinking_toggled_hotkey', {
- enabled
- });
- addNotification({
- key: 'thinking-toggled-hotkey',
- jsx:
+ const handleThinkingSelect = useCallback(
+ (enabled: boolean) => {
+ setAppState(prev => ({
+ ...prev,
+ thinkingEnabled: enabled,
+ }))
+ setShowThinkingToggle(false)
+ logEvent('tengu_thinking_toggled_hotkey', { enabled })
+ addNotification({
+ key: 'thinking-toggled-hotkey',
+ jsx: (
+
Thinking {enabled ? 'on' : 'off'}
- ,
- priority: 'immediate',
- timeoutMs: 3000
- });
- }, [setAppState, addNotification]);
+
+ ),
+ priority: 'immediate',
+ timeoutMs: 3000,
+ })
+ },
+ [setAppState, addNotification],
+ )
+
const handleThinkingCancel = useCallback(() => {
- setShowThinkingToggle(false);
- }, []);
+ setShowThinkingToggle(false)
+ }, [])
// Memoize the thinking toggle element
const thinkingToggleElement = useMemo(() => {
- if (!showThinkingToggle) return null;
- return
- m.type === 'assistant')} />
- ;
- }, [showThinkingToggle, thinkingEnabled, handleThinkingSelect, handleThinkingCancel, messages.length]);
+ if (!showThinkingToggle) return null
+ return (
+
+ m.type === 'assistant')}
+ />
+
+ )
+ }, [
+ showThinkingToggle,
+ thinkingEnabled,
+ handleThinkingSelect,
+ handleThinkingCancel,
+ messages.length,
+ ])
// Portal dialog to DialogOverlay in fullscreen so it escapes the bottom
// slot's overflowY:hidden clip (same pattern as SuggestionsOverlay).
// Must be called before early returns below to satisfy rules-of-hooks.
// Memoized so the portal useEffect doesn't churn on every PromptInput render.
- const autoModeOptInDialog = useMemo(() => feature('TRANSCRIPT_CLASSIFIER') && showAutoModeOptIn ? : null, [showAutoModeOptIn, handleAutoModeOptInAccept, handleAutoModeOptInDecline]);
- useSetPromptOverlayDialog(isFullscreenEnvEnabled() ? autoModeOptInDialog : null);
+ const autoModeOptInDialog = useMemo(
+ () =>
+ feature('TRANSCRIPT_CLASSIFIER') && showAutoModeOptIn ? (
+
+ ) : null,
+ [showAutoModeOptIn, handleAutoModeOptInAccept, handleAutoModeOptInDecline],
+ )
+ useSetPromptOverlayDialog(
+ isFullscreenEnvEnabled() ? autoModeOptInDialog : null,
+ )
+
if (showBashesDialog) {
- return setShowBashesDialog(false)} toolUseContext={getToolUseContext(messages, [], new AbortController(), mainLoopModel)} initialDetailTaskId={typeof showBashesDialog === 'string' ? showBashesDialog : undefined} />;
+ return (
+ setShowBashesDialog(false)}
+ toolUseContext={getToolUseContext(
+ messages,
+ [],
+ new AbortController(),
+ mainLoopModel,
+ )}
+ initialDetailTaskId={
+ typeof showBashesDialog === 'string' ? showBashesDialog : undefined
+ }
+ />
+ )
}
+
if (isAgentSwarmsEnabled() && showTeamsDialog) {
- return {
- setShowTeamsDialog(false);
- }} />;
+ return (
+ {
+ setShowTeamsDialog(false)
+ }}
+ />
+ )
}
+
if (feature('QUICK_SEARCH')) {
const insertWithSpacing = (text: string) => {
- const cursorChar = input[cursorOffset - 1] ?? ' ';
- insertTextAtCursor(/\s/.test(cursorChar) ? text : ` ${text}`);
- };
+ const cursorChar = input[cursorOffset - 1] ?? ' '
+ insertTextAtCursor(/\s/.test(cursorChar) ? text : ` ${text}`)
+ }
if (showQuickOpen) {
- return setShowQuickOpen(false)} onInsert={insertWithSpacing} />;
+ return (
+ setShowQuickOpen(false)}
+ onInsert={insertWithSpacing}
+ />
+ )
}
if (showGlobalSearch) {
- return setShowGlobalSearch(false)} onInsert={insertWithSpacing} />;
+ return (
+ setShowGlobalSearch(false)}
+ onInsert={insertWithSpacing}
+ />
+ )
}
}
+
if (feature('HISTORY_PICKER') && showHistoryPicker) {
- return {
- const entryMode = getModeFromInput(entry.display);
- const value = getValueFromInput(entry.display);
- onModeChange(entryMode);
- trackAndSetInput(value);
- setPastedContents(entry.pastedContents);
- setCursorOffset(value.length);
- setShowHistoryPicker(false);
- }} onCancel={() => setShowHistoryPicker(false)} />;
+ return (
+ {
+ const entryMode = getModeFromInput(entry.display)
+ const value = getValueFromInput(entry.display)
+ onModeChange(entryMode)
+ trackAndSetInput(value)
+ setPastedContents(entry.pastedContents)
+ setCursorOffset(value.length)
+ setShowHistoryPicker(false)
+ }}
+ onCancel={() => setShowHistoryPicker(false)}
+ />
+ )
}
// Show loop mode menu when requested (ant-only, eliminated from external builds)
if (modelPickerElement) {
- return modelPickerElement;
+ return modelPickerElement
}
+
if (fastModePickerElement) {
- return fastModePickerElement;
+ return fastModePickerElement
}
+
if (thinkingToggleElement) {
- return thinkingToggleElement;
+ return thinkingToggleElement
}
+
if (showBridgeDialog) {
- return {
- setShowBridgeDialog(false);
- selectFooterItem(null);
- }} />;
+ return (
+ {
+ setShowBridgeDialog(false)
+ selectFooterItem(null)
+ }}
+ />
+ )
}
+
const baseProps: BaseTextInputProps = {
multiline: true,
onSubmit,
onChange,
- value: historyMatch ? getValueFromInput(typeof historyMatch === 'string' ? historyMatch : historyMatch.display) : input,
+ value: historyMatch
+ ? getValueFromInput(
+ typeof historyMatch === 'string'
+ ? historyMatch
+ : historyMatch.display,
+ )
+ : input,
// History navigation is handled via TextInput props (onHistoryUp/onHistoryDown),
// NOT via useKeybindings. This allows useTextInput's upOrHistoryUp/downOrHistoryDown
// to try cursor movement first and only fall through to history navigation when the
@@ -2183,117 +2888,243 @@ function PromptInput({
onHistoryReset: resetHistory,
placeholder,
onExit,
- onExitMessage: (show, key) => setExitMessage({
- show,
- key
- }),
+ onExitMessage: (show, key) => setExitMessage({ show, key }),
onImagePaste,
columns: textInputColumns,
maxVisibleLines,
- disableCursorMovementForUpDownKeys: suggestions.length > 0 || !!footerItemSelected,
+ disableCursorMovementForUpDownKeys:
+ suggestions.length > 0 || !!footerItemSelected,
disableEscapeDoublePress: suggestions.length > 0,
cursorOffset,
onChangeCursorOffset: setCursorOffset,
onPaste: onTextPaste,
onIsPastingChange: setIsPasting,
focus: !isSearchingHistory && !isModalOverlayActive && !footerItemSelected,
- showCursor: !footerItemSelected && !isSearchingHistory && !cursorAtImageChip,
+ showCursor:
+ !footerItemSelected && !isSearchingHistory && !cursorAtImageChip,
argumentHint: commandArgumentHint,
- onUndo: canUndo ? () => {
- const previousState = undo();
- if (previousState) {
- trackAndSetInput(previousState.text);
- setCursorOffset(previousState.cursorOffset);
- setPastedContents(previousState.pastedContents);
- }
- } : undefined,
+ onUndo: canUndo
+ ? () => {
+ const previousState = undo()
+ if (previousState) {
+ trackAndSetInput(previousState.text)
+ setCursorOffset(previousState.cursorOffset)
+ setPastedContents(previousState.pastedContents)
+ }
+ }
+ : undefined,
highlights: combinedHighlights,
inlineGhostText,
- inputFilter: lazySpaceInputFilter
- };
+ inputFilter: lazySpaceInputFilter,
+ }
+
const getBorderColor = (): keyof Theme => {
const modeColors: Record = {
- bash: 'bashBorder'
- };
+ bash: 'bashBorder',
+ }
// Mode colors take priority, then teammate color, then default
if (modeColors[mode]) {
- return modeColors[mode];
+ return modeColors[mode]
}
// In-process teammates run headless - don't apply teammate colors to leader UI
if (isInProcessTeammate()) {
- return 'promptBorder';
+ return 'promptBorder'
}
// Check for teammate color from environment
- const teammateColorName = getTeammateColor();
- if (teammateColorName && AGENT_COLORS.includes(teammateColorName as AgentColorName)) {
- return AGENT_COLOR_TO_THEME_COLOR[teammateColorName as AgentColorName];
+ const teammateColorName = getTeammateColor()
+ if (
+ teammateColorName &&
+ AGENT_COLORS.includes(teammateColorName as AgentColorName)
+ ) {
+ return AGENT_COLOR_TO_THEME_COLOR[teammateColorName as AgentColorName]
}
- return 'promptBorder';
- };
+
+ return 'promptBorder'
+ }
+
if (isExternalEditorActive) {
- return
+ return (
+
Save and close editor to continue...
- ;
+
+ )
}
- const textInputElement = isVimModeEnabled() ? : ;
- return
+
+ const textInputElement = isVimModeEnabled() ? (
+
+ ) : (
+
+ )
+
+ return (
+
{!isFullscreenEnvEnabled() && }
- {hasSuppressedDialogs &&
+ {hasSuppressedDialogs && (
+
Waiting for permission…
- }
+
+ )}
- {swarmBanner ? <>
+ {swarmBanner ? (
+ <>
- {swarmBanner.text ? <>
- {'─'.repeat(Math.max(0, columns - stringWidth(swarmBanner.text) - 4))}
+ {swarmBanner.text ? (
+ <>
+ {'─'.repeat(
+ Math.max(0, columns - stringWidth(swarmBanner.text) - 4),
+ )}
{' '}
{swarmBanner.text}{' '}
{'──'}
- > : '─'.repeat(columns)}
+ >
+ ) : (
+ '─'.repeat(columns)
+ )}
-
+
{textInputElement}
{'─'.repeat(columns)}
- > :
-
+ >
+ ) : (
+
+
{textInputElement}
- }
- 0} isLoading={isLoading} tasksSelected={tasksSelected} teamsSelected={teamsSelected} bridgeSelected={bridgeSelected} tmuxSelected={tmuxSelected} teammateFooterIndex={teammateFooterIndex} ideSelection={ideSelection} mcpClients={mcpClients} isPasting={isPasting} isInputWrapped={isInputWrapped} messages={messages} isSearching={isSearchingHistory} historyQuery={historyQuery} setHistoryQuery={setHistoryQuery} historyFailedMatch={historyFailedMatch} onOpenTasksDialog={isFullscreenEnvEnabled() ? handleOpenTasksDialog : undefined} />
+
+ )}
+ 0}
+ isLoading={isLoading}
+ tasksSelected={tasksSelected}
+ teamsSelected={teamsSelected}
+ bridgeSelected={bridgeSelected}
+ tmuxSelected={tmuxSelected}
+ teammateFooterIndex={teammateFooterIndex}
+ ideSelection={ideSelection}
+ mcpClients={mcpClients}
+ isPasting={isPasting}
+ isInputWrapped={isInputWrapped}
+ messages={messages}
+ isSearching={isSearchingHistory}
+ historyQuery={historyQuery}
+ setHistoryQuery={setHistoryQuery}
+ historyFailedMatch={historyFailedMatch}
+ onOpenTasksDialog={
+ isFullscreenEnvEnabled() ? handleOpenTasksDialog : undefined
+ }
+ />
{isFullscreenEnvEnabled() ? null : autoModeOptInDialog}
- {isFullscreenEnvEnabled() ?
- // position=absolute takes zero layout height so the spinner
- // doesn't shift when a notification appears/disappears. Yoga
- // anchors absolute children at the parent's content-box origin;
- // marginTop=-1 pulls it into the marginTop=1 gap row above the
- // prompt border. In brief mode there is no such gap (briefOwnsGap
- // strips our marginTop) and BriefSpinner sits flush against the
- // border — marginTop=-2 skips over the spinner content into
- // BriefSpinner's own marginTop=1 blank row. height=1 +
- // overflow=hidden clips multi-line notifications to a single row.
- // flex-end anchors the bottom line so the visible row is always
- // the most recent. Suppressed while the slash overlay or
- // auto-mode opt-in dialog is up by height=0 (NOT unmount) — this
- // Box renders later in tree order so it would paint over their
- // bottom row. Keeping Notifications mounted prevents AutoUpdater's
- // initial-check effect from re-firing on every slash-completion
- // toggle (PR#22413).
-
-
- : null}
- ;
+ {isFullscreenEnvEnabled() ? (
+ // position=absolute takes zero layout height so the spinner
+ // doesn't shift when a notification appears/disappears. Yoga
+ // anchors absolute children at the parent's content-box origin;
+ // marginTop=-1 pulls it into the marginTop=1 gap row above the
+ // prompt border. In brief mode there is no such gap (briefOwnsGap
+ // strips our marginTop) and BriefSpinner sits flush against the
+ // border — marginTop=-2 skips over the spinner content into
+ // BriefSpinner's own marginTop=1 blank row. height=1 +
+ // overflow=hidden clips multi-line notifications to a single row.
+ // flex-end anchors the bottom line so the visible row is always
+ // the most recent. Suppressed while the slash overlay or
+ // auto-mode opt-in dialog is up by height=0 (NOT unmount) — this
+ // Box renders later in tree order so it would paint over their
+ // bottom row. Keeping Notifications mounted prevents AutoUpdater's
+ // initial-check effect from re-firing on every slash-completion
+ // toggle (PR#22413).
+
+
+
+ ) : null}
+
+ )
}
/**
@@ -2301,38 +3132,46 @@ function PromptInput({
* This handles --continue/--resume scenarios where we need to avoid ID collisions.
*/
function getInitialPasteId(messages: Message[]): number {
- let maxId = 0;
+ let maxId = 0
for (const message of messages) {
if (message.type === 'user') {
// Check image paste IDs
if (message.imagePasteIds) {
- for (const id of message.imagePasteIds as number[]) {
- if (id > maxId) maxId = id;
+ for (const id of message.imagePasteIds) {
+ if (id > maxId) maxId = id
}
}
// Check text paste references in message content
if (Array.isArray(message.message.content)) {
for (const block of message.message.content) {
if (block.type === 'text') {
- const refs = parseReferences(block.text);
+ const refs = parseReferences(block.text)
for (const ref of refs) {
- if (ref.id > maxId) maxId = ref.id;
+ if (ref.id > maxId) maxId = ref.id
}
}
}
}
}
}
- return maxId + 1;
+ return maxId + 1
}
-function buildBorderText(showFastIcon: boolean, showFastIconHint: boolean, fastModeCooldown: boolean): BorderTextOptions | undefined {
- if (!showFastIcon) return undefined;
- const fastSeg = showFastIconHint ? `${getFastIconString(true, fastModeCooldown)} ${chalk.dim('/fast')}` : getFastIconString(true, fastModeCooldown);
+
+function buildBorderText(
+ showFastIcon: boolean,
+ showFastIconHint: boolean,
+ fastModeCooldown: boolean,
+): BorderTextOptions | undefined {
+ if (!showFastIcon) return undefined
+ const fastSeg = showFastIconHint
+ ? `${getFastIconString(true, fastModeCooldown)} ${chalk.dim('/fast')}`
+ : getFastIconString(true, fastModeCooldown)
return {
content: ` ${fastSeg} `,
position: 'top',
align: 'end',
- offset: 0
- };
+ offset: 0,
+ }
}
-export default React.memo(PromptInput);
+
+export default React.memo(PromptInput)
diff --git a/src/components/PromptInput/PromptInputFooter.tsx b/src/components/PromptInput/PromptInputFooter.tsx
index 8f23dfdb9..652bdf3f0 100644
--- a/src/components/PromptInput/PromptInputFooter.tsx
+++ b/src/components/PromptInput/PromptInputFooter.tsx
@@ -1,65 +1,77 @@
-import { feature } from 'bun:bundle';
-import * as React from 'react';
-import { memo, type ReactNode, useMemo, useRef } from 'react';
-import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js';
-import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js';
-import { useSetPromptOverlay } from '../../context/promptOverlayContext.js';
-import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js';
-import type { IDESelection } from '../../hooks/useIdeSelection.js';
-import { useSettings } from '../../hooks/useSettings.js';
-import { useTerminalSize } from '../../hooks/useTerminalSize.js';
-import { Box, Text } from '../../ink.js';
-import type { MCPServerConnection } from '../../services/mcp/types.js';
-import { useAppState } from '../../state/AppState.js';
-import type { ToolPermissionContext } from '../../Tool.js';
-import type { Message } from '../../types/message.js';
-import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js';
-import type { AutoUpdaterResult } from '../../utils/autoUpdater.js';
-import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js';
-import { isUndercover } from '../../utils/undercover.js';
-import { CoordinatorTaskPanel, useCoordinatorTaskCount } from '../CoordinatorAgentStatus.js';
-import { getLastAssistantMessageId, StatusLine, statusLineShouldDisplay } from '../StatusLine.js';
-import { Notifications } from './Notifications.js';
-import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js';
-import { PromptInputFooterSuggestions, type SuggestionItem } from './PromptInputFooterSuggestions.js';
-import { PromptInputHelpMenu } from './PromptInputHelpMenu.js';
+import { feature } from 'bun:bundle'
+import * as React from 'react'
+import { memo, type ReactNode, useMemo, useRef } from 'react'
+import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js'
+import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js'
+import { useSetPromptOverlay } from '../../context/promptOverlayContext.js'
+import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
+import type { IDESelection } from '../../hooks/useIdeSelection.js'
+import { useSettings } from '../../hooks/useSettings.js'
+import { useTerminalSize } from '../../hooks/useTerminalSize.js'
+import { Box, Text } from '../../ink.js'
+import type { MCPServerConnection } from '../../services/mcp/types.js'
+import { useAppState } from '../../state/AppState.js'
+import type { ToolPermissionContext } from '../../Tool.js'
+import type { Message } from '../../types/message.js'
+import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js'
+import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
+import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
+import { isUndercover } from '../../utils/undercover.js'
+import {
+ CoordinatorTaskPanel,
+ useCoordinatorTaskCount,
+} from '../CoordinatorAgentStatus.js'
+import {
+ getLastAssistantMessageId,
+ StatusLine,
+ statusLineShouldDisplay,
+} from '../StatusLine.js'
+import { Notifications } from './Notifications.js'
+import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js'
+import {
+ PromptInputFooterSuggestions,
+ type SuggestionItem,
+} from './PromptInputFooterSuggestions.js'
+import { PromptInputHelpMenu } from './PromptInputHelpMenu.js'
+
type Props = {
- apiKeyStatus: VerificationStatus;
- debug: boolean;
+ apiKeyStatus: VerificationStatus
+ debug: boolean
exitMessage: {
- show: boolean;
- key?: string;
- };
- vimMode: VimMode | undefined;
- mode: PromptInputMode;
- autoUpdaterResult: AutoUpdaterResult | null;
- isAutoUpdating: boolean;
- verbose: boolean;
- onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
- onChangeIsUpdating: (isUpdating: boolean) => void;
- suggestions: SuggestionItem[];
- selectedSuggestion: number;
- maxColumnWidth?: number;
- toolPermissionContext: ToolPermissionContext;
- helpOpen: boolean;
- suppressHint: boolean;
- isLoading: boolean;
- tasksSelected: boolean;
- teamsSelected: boolean;
- bridgeSelected: boolean;
- tmuxSelected: boolean;
- teammateFooterIndex?: number;
- ideSelection: IDESelection | undefined;
- mcpClients?: MCPServerConnection[];
- isPasting?: boolean;
- isInputWrapped?: boolean;
- messages: Message[];
- isSearching: boolean;
- historyQuery: string;
- setHistoryQuery: (query: string) => void;
- historyFailedMatch: boolean;
- onOpenTasksDialog?: (taskId?: string) => void;
-};
+ show: boolean
+ key?: string
+ }
+ vimMode: VimMode | undefined
+ mode: PromptInputMode
+ autoUpdaterResult: AutoUpdaterResult | null
+ isAutoUpdating: boolean
+ verbose: boolean
+ onAutoUpdaterResult: (result: AutoUpdaterResult) => void
+ onChangeIsUpdating: (isUpdating: boolean) => void
+ suggestions: SuggestionItem[]
+ selectedSuggestion: number
+ maxColumnWidth?: number
+ toolPermissionContext: ToolPermissionContext
+ helpOpen: boolean
+ suppressHint: boolean
+ isLoading: boolean
+ tasksSelected: boolean
+ teamsSelected: boolean
+ bridgeSelected: boolean
+ tmuxSelected: boolean
+ teammateFooterIndex?: number
+ ideSelection: IDESelection | undefined
+ mcpClients?: MCPServerConnection[]
+ isPasting?: boolean
+ isInputWrapped?: boolean
+ messages: Message[]
+ isSearching: boolean
+ historyQuery: string
+ setHistoryQuery: (query: string) => void
+ historyFailedMatch: boolean
+ onOpenTasksDialog?: (taskId?: string) => void
+}
+
function PromptInputFooter({
apiKeyStatus,
debug,
@@ -92,99 +104,176 @@ function PromptInputFooter({
historyQuery,
setHistoryQuery,
historyFailedMatch,
- onOpenTasksDialog
+ onOpenTasksDialog,
}: Props): ReactNode {
- const settings = useSettings();
- const {
- columns,
- rows
- } = useTerminalSize();
- const messagesRef = useRef(messages);
- messagesRef.current = messages;
- const lastAssistantMessageId = useMemo(() => getLastAssistantMessageId(messages), [messages]);
- const isNarrow = columns < 80;
+ const settings = useSettings()
+ const { columns, rows } = useTerminalSize()
+ const messagesRef = useRef(messages)
+ messagesRef.current = messages
+ const lastAssistantMessageId = useMemo(
+ () => getLastAssistantMessageId(messages),
+ [messages],
+ )
+ const isNarrow = columns < 80
// In fullscreen the bottom slot is flexShrink:0, so every row here is a row
// stolen from the ScrollBox. Drop the optional StatusLine first. Non-fullscreen
// has terminal scrollback to absorb overflow, so we never hide StatusLine there.
- const isFullscreen = isFullscreenEnvEnabled();
- const isShort = isFullscreen && rows < 24;
+ const isFullscreen = isFullscreenEnvEnabled()
+ const isShort = isFullscreen && rows < 24
// Pill highlights when tasks is the active footer item AND no specific
// agent row is selected. When coordinatorTaskIndex >= 0 the pointer has
// moved into CoordinatorTaskPanel, so the pill should un-highlight.
// coordinatorTaskCount === 0 covers the bash-only case (no agent rows
// exist, pill is the only selectable item).
- const coordinatorTaskCount = useCoordinatorTaskCount();
- const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex);
- const pillSelected = tasksSelected && (coordinatorTaskCount === 0 || coordinatorTaskIndex < 0);
+ const coordinatorTaskCount = useCoordinatorTaskCount()
+ const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex)
+ const pillSelected =
+ tasksSelected && (coordinatorTaskCount === 0 || coordinatorTaskIndex < 0)
// Hide `? for shortcuts` if the user has a custom status line, or during ctrl-r
- const suppressHint = suppressHintFromProps || statusLineShouldDisplay(settings) || isSearching;
+ const suppressHint =
+ suppressHintFromProps || statusLineShouldDisplay(settings) || isSearching
// Fullscreen: portal data to FullscreenLayout — see promptOverlayContext.tsx
- const overlayData = useMemo(() => isFullscreen && suggestions.length ? {
- suggestions,
- selectedSuggestion,
- maxColumnWidth
- } : null, [isFullscreen, suggestions, selectedSuggestion, maxColumnWidth]);
- useSetPromptOverlay(overlayData);
+ const overlayData = useMemo(
+ () =>
+ isFullscreen && suggestions.length
+ ? { suggestions, selectedSuggestion, maxColumnWidth }
+ : null,
+ [isFullscreen, suggestions, selectedSuggestion, maxColumnWidth],
+ )
+ useSetPromptOverlay(overlayData)
+
if (suggestions.length && !isFullscreen) {
- return
-
- ;
+ return (
+
+
+
+ )
}
+
if (helpOpen) {
- return ;
+ return (
+
+ )
}
- return <>
-
+
+ return (
+ <>
+
- {mode === 'prompt' && !isShort && !exitMessage.show && !isPasting && statusLineShouldDisplay(settings) && }
-
+ {mode === 'prompt' &&
+ !isShort &&
+ !exitMessage.show &&
+ !isPasting &&
+ statusLineShouldDisplay(settings) && (
+
+ )}
+
- {isFullscreen ? null : }
- {(process.env.USER_TYPE) === 'ant' && isUndercover() && undercover}
+ {isFullscreen ? null : (
+
+ )}
+ {process.env.USER_TYPE === 'ant' && isUndercover() && (
+ undercover
+ )}
- {(process.env.USER_TYPE) === 'ant' && }
- >;
+ {process.env.USER_TYPE === 'ant' && }
+ >
+ )
}
-export default memo(PromptInputFooter);
+
+export default memo(PromptInputFooter)
+
type BridgeStatusProps = {
- bridgeSelected: boolean;
-};
+ bridgeSelected: boolean
+}
+
function BridgeStatusIndicator({
- bridgeSelected
+ bridgeSelected,
}: BridgeStatusProps): React.ReactNode {
- if (!feature('BRIDGE_MODE')) return null;
+ if (!feature('BRIDGE_MODE')) return null
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- const enabled = useAppState(s => s.replBridgeEnabled);
+ const enabled = useAppState(s => s.replBridgeEnabled)
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- const connected = useAppState(s_0 => s_0.replBridgeConnected);
+ const connected = useAppState(s => s.replBridgeConnected)
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- const sessionActive = useAppState(s_1 => s_1.replBridgeSessionActive);
+ const sessionActive = useAppState(s => s.replBridgeSessionActive)
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- const reconnecting = useAppState(s_2 => s_2.replBridgeReconnecting);
+ const reconnecting = useAppState(s => s.replBridgeReconnecting)
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- const explicit = useAppState(s_3 => s_3.replBridgeExplicit);
+ const explicit = useAppState(s => s.replBridgeExplicit)
// Failed state is surfaced via notification (useReplBridge), not a footer pill.
- if (!isBridgeEnabled() || !enabled) return null;
+ if (!isBridgeEnabled() || !enabled) return null
+
const status = getBridgeStatus({
error: undefined,
connected,
sessionActive,
- reconnecting
- });
+ reconnecting,
+ })
// For implicit (config-driven) remote, only show the reconnecting state
if (!explicit && status.label !== 'Remote Control reconnecting') {
- return null;
+ return null
}
- return
+
+ return (
+
{status.label}
{bridgeSelected && · Enter to view}
- ;
+
+ )
}
diff --git a/src/components/PromptInput/PromptInputFooterLeftSide.tsx b/src/components/PromptInput/PromptInputFooterLeftSide.tsx
index 9480af9eb..fc1be8124 100644
--- a/src/components/PromptInput/PromptInputFooterLeftSide.tsx
+++ b/src/components/PromptInput/PromptInputFooterLeftSide.tsx
@@ -1,239 +1,207 @@
-import { c as _c } from "react/compiler-runtime";
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
-import { feature } from 'bun:bundle';
+import { feature } from 'bun:bundle'
// Dead code elimination: conditional import for COORDINATOR_MODE
/* eslint-disable @typescript-eslint/no-require-imports */
-const coordinatorModule = feature('COORDINATOR_MODE') ? require('../../coordinator/coordinatorMode.js') as typeof import('../../coordinator/coordinatorMode.js') : undefined;
+const coordinatorModule = feature('COORDINATOR_MODE')
+ ? (require('../../coordinator/coordinatorMode.js') as typeof import('../../coordinator/coordinatorMode.js'))
+ : undefined
/* eslint-enable @typescript-eslint/no-require-imports */
-import { Box, Text, Link } from '../../ink.js';
-import * as React from 'react';
-import figures from 'figures';
-import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
-import type { VimMode, PromptInputMode } from '../../types/textInputTypes.js';
-import type { ToolPermissionContext } from '../../Tool.js';
-import { isVimModeEnabled } from './utils.js';
-import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
-import { isDefaultMode, permissionModeSymbol, permissionModeTitle, getModeColor } from '../../utils/permissions/PermissionMode.js';
-import { BackgroundTaskStatus } from '../tasks/BackgroundTaskStatus.js';
-import { isBackgroundTask } from '../../tasks/types.js';
-import { isPanelAgentTask } from '../../tasks/LocalAgentTask/LocalAgentTask.js';
-import { getVisibleAgentTasks } from '../CoordinatorAgentStatus.js';
-import { count } from '../../utils/array.js';
-import { shouldHideTasksFooter } from '../tasks/taskStatusUtils.js';
-import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js';
-import { TeamStatus } from '../teams/TeamStatus.js';
-import { isInProcessEnabled } from '../../utils/swarm/backends/registry.js';
-import { useAppState, useAppStateStore } from 'src/state/AppState.js';
-import { getIsRemoteMode } from '../../bootstrap/state.js';
-import HistorySearchInput from './HistorySearchInput.js';
-import { usePrStatus } from '../../hooks/usePrStatus.js';
-import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
-import { Byline } from '../design-system/Byline.js';
-import { useTerminalSize } from '../../hooks/useTerminalSize.js';
-import { useTasksV2 } from '../../hooks/useTasksV2.js';
-import { formatDuration } from '../../utils/format.js';
-import { VoiceWarmupHint } from './VoiceIndicator.js';
-import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js';
-import { useVoiceState } from '../../context/voice.js';
-import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js';
-import { isXtermJs } from '../../ink/terminal.js';
-import { useHasSelection, useSelection } from '../../ink/hooks/use-selection.js';
-import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
-import { getPlatform } from '../../utils/platform.js';
-import { PrBadge } from '../PrBadge.js';
+import { Box, Text, Link } from '../../ink.js'
+import * as React from 'react'
+import figures from 'figures'
+import {
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ useSyncExternalStore,
+} from 'react'
+import type { VimMode, PromptInputMode } from '../../types/textInputTypes.js'
+import type { ToolPermissionContext } from '../../Tool.js'
+import { isVimModeEnabled } from './utils.js'
+import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
+import {
+ isDefaultMode,
+ permissionModeSymbol,
+ permissionModeTitle,
+ getModeColor,
+} from '../../utils/permissions/PermissionMode.js'
+import { BackgroundTaskStatus } from '../tasks/BackgroundTaskStatus.js'
+import { isBackgroundTask } from '../../tasks/types.js'
+import { isPanelAgentTask } from '../../tasks/LocalAgentTask/LocalAgentTask.js'
+import { getVisibleAgentTasks } from '../CoordinatorAgentStatus.js'
+import { count } from '../../utils/array.js'
+import { shouldHideTasksFooter } from '../tasks/taskStatusUtils.js'
+import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
+import { TeamStatus } from '../teams/TeamStatus.js'
+import { isInProcessEnabled } from '../../utils/swarm/backends/registry.js'
+import { useAppState, useAppStateStore } from 'src/state/AppState.js'
+import { getIsRemoteMode } from '../../bootstrap/state.js'
+import HistorySearchInput from './HistorySearchInput.js'
+import { usePrStatus } from '../../hooks/usePrStatus.js'
+import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
+import { Byline } from '../design-system/Byline.js'
+import { useTerminalSize } from '../../hooks/useTerminalSize.js'
+import { useTasksV2 } from '../../hooks/useTasksV2.js'
+import { formatDuration } from '../../utils/format.js'
+import { VoiceWarmupHint } from './VoiceIndicator.js'
+import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'
+import { useVoiceState } from '../../context/voice.js'
+import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
+import { isXtermJs } from '../../ink/terminal.js'
+import { useHasSelection, useSelection } from '../../ink/hooks/use-selection.js'
+import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
+import { getPlatform } from '../../utils/platform.js'
+import { PrBadge } from '../PrBadge.js'
// Dead code elimination: conditional import for proactive mode
/* eslint-disable @typescript-eslint/no-require-imports */
-const proactiveModule = feature('PROACTIVE') || feature('KAIROS') ? require('../../proactive/index.js') : null;
+const proactiveModule =
+ feature('PROACTIVE') || feature('KAIROS')
+ ? require('../../proactive/index.js')
+ : null
/* eslint-enable @typescript-eslint/no-require-imports */
-const NO_OP_SUBSCRIBE = (_cb: () => void) => () => {};
-const NULL = () => null;
-const MAX_VOICE_HINT_SHOWS = 3;
+const NO_OP_SUBSCRIBE = (_cb: () => void) => () => {}
+const NULL = () => null
+const MAX_VOICE_HINT_SHOWS = 3
+
type Props = {
exitMessage: {
- show: boolean;
- key?: string;
- };
- vimMode: VimMode | undefined;
- mode: PromptInputMode;
- toolPermissionContext: ToolPermissionContext;
- suppressHint: boolean;
- isLoading: boolean;
- showMemoryTypeSelector?: boolean;
- tasksSelected: boolean;
- teamsSelected: boolean;
- tmuxSelected: boolean;
- teammateFooterIndex?: number;
- isPasting?: boolean;
- isSearching: boolean;
- historyQuery: string;
- setHistoryQuery: (query: string) => void;
- historyFailedMatch: boolean;
- onOpenTasksDialog?: (taskId?: string) => void;
-};
-function ProactiveCountdown() {
- const $ = _c(7);
- const nextTickAt = useSyncExternalStore(proactiveModule?.subscribeToProactiveChanges ?? NO_OP_SUBSCRIBE, proactiveModule?.getNextTickAt ?? NULL, NULL);
- const [remainingSeconds, setRemainingSeconds] = useState(null);
- let t0;
- let t1;
- if ($[0] !== nextTickAt) {
- t0 = () => {
- if (nextTickAt === null) {
- setRemainingSeconds(null);
- return;
- }
- const update = function update() {
- const remaining = Math.max(0, Math.ceil((nextTickAt - Date.now()) / 1000));
- setRemainingSeconds(remaining);
- };
- update();
- const interval = setInterval(update, 1000);
- return () => clearInterval(interval);
- };
- t1 = [nextTickAt];
- $[0] = nextTickAt;
- $[1] = t0;
- $[2] = t1;
- } else {
- t0 = $[1];
- t1 = $[2];
+ show: boolean
+ key?: string
}
- useEffect(t0, t1);
- if (remainingSeconds === null) {
- return null;
- }
- const t2 = remainingSeconds * 1000;
- let t3;
- if ($[3] !== t2) {
- t3 = formatDuration(t2, {
- mostSignificantOnly: true
- });
- $[3] = t2;
- $[4] = t3;
- } else {
- t3 = $[4];
- }
- let t4;
- if ($[5] !== t3) {
- t4 = waiting{" "}{t3};
- $[5] = t3;
- $[6] = t4;
- } else {
- t4 = $[6];
- }
- return t4;
+ vimMode: VimMode | undefined
+ mode: PromptInputMode
+ toolPermissionContext: ToolPermissionContext
+ suppressHint: boolean
+ isLoading: boolean
+ showMemoryTypeSelector?: boolean
+ tasksSelected: boolean
+ teamsSelected: boolean
+ tmuxSelected: boolean
+ teammateFooterIndex?: number
+ isPasting?: boolean
+ isSearching: boolean
+ historyQuery: string
+ setHistoryQuery: (query: string) => void
+ historyFailedMatch: boolean
+ onOpenTasksDialog?: (taskId?: string) => void
}
-export function PromptInputFooterLeftSide(t0) {
- const $ = _c(27);
- const {
- exitMessage,
- vimMode,
- mode,
- toolPermissionContext,
- suppressHint,
- isLoading,
- tasksSelected,
- teamsSelected,
- tmuxSelected,
- teammateFooterIndex,
- isPasting,
- isSearching,
- historyQuery,
- setHistoryQuery,
- historyFailedMatch,
- onOpenTasksDialog
- } = t0;
- if (exitMessage.show) {
- let t1;
- if ($[0] !== exitMessage.key) {
- t1 = Press {exitMessage.key} again to exit;
- $[0] = exitMessage.key;
- $[1] = t1;
- } else {
- t1 = $[1];
+
+function ProactiveCountdown(): React.ReactNode {
+ const nextTickAt = useSyncExternalStore(
+ proactiveModule?.subscribeToProactiveChanges ?? NO_OP_SUBSCRIBE,
+ proactiveModule?.getNextTickAt ?? NULL,
+ NULL,
+ )
+
+ const [remainingSeconds, setRemainingSeconds] = useState(null)
+
+ useEffect(() => {
+ if (nextTickAt === null) {
+ setRemainingSeconds(null)
+ return
}
- return t1;
+
+ function update(): void {
+ const remaining = Math.max(
+ 0,
+ Math.ceil((nextTickAt! - Date.now()) / 1000),
+ )
+ setRemainingSeconds(remaining)
+ }
+
+ update()
+ const interval = setInterval(update, 1000)
+ return () => clearInterval(interval)
+ }, [nextTickAt])
+
+ if (remainingSeconds === null) return null
+
+ return (
+
+ waiting{' '}
+ {formatDuration(remainingSeconds * 1000, { mostSignificantOnly: true })}
+
+ )
+}
+
+export function PromptInputFooterLeftSide({
+ exitMessage,
+ vimMode,
+ mode,
+ toolPermissionContext,
+ suppressHint,
+ isLoading,
+ tasksSelected,
+ teamsSelected,
+ tmuxSelected,
+ teammateFooterIndex,
+ isPasting,
+ isSearching,
+ historyQuery,
+ setHistoryQuery,
+ historyFailedMatch,
+ onOpenTasksDialog,
+}: Props): React.ReactNode {
+ if (exitMessage.show) {
+ return (
+
+ Press {exitMessage.key} again to exit
+
+ )
}
if (isPasting) {
- let t1;
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = Pasting text…;
- $[2] = t1;
- } else {
- t1 = $[2];
- }
- return t1;
+ return (
+
+ Pasting text…
+
+ )
}
- let t1;
- if ($[3] !== isSearching || $[4] !== vimMode) {
- t1 = isVimModeEnabled() && vimMode === "INSERT" && !isSearching;
- $[3] = isSearching;
- $[4] = vimMode;
- $[5] = t1;
- } else {
- t1 = $[5];
- }
- const showVim = t1;
- let t2;
- if ($[6] !== historyFailedMatch || $[7] !== historyQuery || $[8] !== isSearching || $[9] !== setHistoryQuery) {
- t2 = isSearching && ;
- $[6] = historyFailedMatch;
- $[7] = historyQuery;
- $[8] = isSearching;
- $[9] = setHistoryQuery;
- $[10] = t2;
- } else {
- t2 = $[10];
- }
- let t3;
- if ($[11] !== showVim) {
- t3 = showVim ? -- INSERT -- : null;
- $[11] = showVim;
- $[12] = t3;
- } else {
- t3 = $[12];
- }
- const t4 = !suppressHint && !showVim;
- let t5;
- if ($[13] !== isLoading || $[14] !== mode || $[15] !== onOpenTasksDialog || $[16] !== t4 || $[17] !== tasksSelected || $[18] !== teammateFooterIndex || $[19] !== teamsSelected || $[20] !== tmuxSelected || $[21] !== toolPermissionContext) {
- t5 = ;
- $[13] = isLoading;
- $[14] = mode;
- $[15] = onOpenTasksDialog;
- $[16] = t4;
- $[17] = tasksSelected;
- $[18] = teammateFooterIndex;
- $[19] = teamsSelected;
- $[20] = tmuxSelected;
- $[21] = toolPermissionContext;
- $[22] = t5;
- } else {
- t5 = $[22];
- }
- let t6;
- if ($[23] !== t2 || $[24] !== t3 || $[25] !== t5) {
- t6 = {t2}{t3}{t5};
- $[23] = t2;
- $[24] = t3;
- $[25] = t5;
- $[26] = t6;
- } else {
- t6 = $[26];
- }
- return t6;
+
+ const showVim = isVimModeEnabled() && vimMode === 'INSERT' && !isSearching
+
+ return (
+
+ {isSearching && (
+
+ )}
+ {showVim ? (
+
+ -- INSERT --
+
+ ) : null}
+
+
+ )
}
+
type ModeIndicatorProps = {
- mode: PromptInputMode;
- toolPermissionContext: ToolPermissionContext;
- showHint: boolean;
- isLoading: boolean;
- tasksSelected: boolean;
- teamsSelected: boolean;
- tmuxSelected: boolean;
- teammateFooterIndex?: number;
- onOpenTasksDialog?: (taskId?: string) => void;
-};
+ mode: PromptInputMode
+ toolPermissionContext: ToolPermissionContext
+ showHint: boolean
+ isLoading: boolean
+ tasksSelected: boolean
+ teamsSelected: boolean
+ tmuxSelected: boolean
+ teammateFooterIndex?: number
+ onOpenTasksDialog?: (taskId?: string) => void
+}
+
function ModeIndicator({
mode,
toolPermissionContext,
@@ -243,186 +211,334 @@ function ModeIndicator({
teamsSelected,
tmuxSelected,
teammateFooterIndex,
- onOpenTasksDialog
+ onOpenTasksDialog,
}: ModeIndicatorProps): React.ReactNode {
- const {
- columns
- } = useTerminalSize();
- const modeCycleShortcut = useShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab');
- const tasks = useAppState(s => s.tasks);
- const teamContext = useAppState(s_0 => s_0.teamContext);
+ const { columns } = useTerminalSize()
+ const modeCycleShortcut = useShortcutDisplay(
+ 'chat:cycleMode',
+ 'Chat',
+ 'shift+tab',
+ )
+ const tasks = useAppState(s => s.tasks)
+ const teamContext = useAppState(s => s.teamContext)
// Set once in initialState (main.tsx --remote mode) and never mutated — lazy
// init captures the immutable value without a subscription.
- const store = useAppStateStore();
- const [remoteSessionUrl] = useState(() => store.getState().remoteSessionUrl);
- const viewSelectionMode = useAppState(s_1 => s_1.viewSelectionMode);
- const viewingAgentTaskId = useAppState(s_2 => s_2.viewingAgentTaskId);
- const expandedView = useAppState(s_3 => s_3.expandedView);
- const showSpinnerTree = expandedView === 'teammates';
- const prStatus = usePrStatus(isLoading, isPrStatusEnabled());
- const hasTmuxSession = useAppState(s_4 => (process.env.USER_TYPE) === 'ant' && s_4.tungstenActiveSession !== undefined);
- const nextTickAt = useSyncExternalStore(proactiveModule?.subscribeToProactiveChanges ?? NO_OP_SUBSCRIBE, proactiveModule?.getNextTickAt ?? NULL, NULL);
+ const store = useAppStateStore()
+ const [remoteSessionUrl] = useState(() => store.getState().remoteSessionUrl)
+ const viewSelectionMode = useAppState(s => s.viewSelectionMode)
+ const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId)
+ const expandedView = useAppState(s => s.expandedView)
+ const showSpinnerTree = expandedView === 'teammates'
+ const prStatus = usePrStatus(isLoading, isPrStatusEnabled())
+ const hasTmuxSession = useAppState(
+ s =>
+ process.env.USER_TYPE === 'ant' && s.tungstenActiveSession !== undefined,
+ )
+
+ const nextTickAt = useSyncExternalStore(
+ proactiveModule?.subscribeToProactiveChanges ?? NO_OP_SUBSCRIBE,
+ proactiveModule?.getNextTickAt ?? NULL,
+ NULL,
+ )
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false;
- const voiceState = feature('VOICE_MODE') ?
- // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- useVoiceState(s_5 => s_5.voiceState) : 'idle' as const;
- const voiceWarmingUp = feature('VOICE_MODE') ?
- // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- useVoiceState(s_6 => s_6.voiceWarmingUp) : false;
- const hasSelection = useHasSelection();
- const selGetState = useSelection().getState;
- const hasNextTick = nextTickAt !== null;
- const isCoordinator = feature('COORDINATOR_MODE') ? coordinatorModule?.isCoordinatorMode() === true : false;
- const runningTaskCount = useMemo(() => count(Object.values(tasks), t => isBackgroundTask(t) && !((process.env.USER_TYPE) === 'ant' && isPanelAgentTask(t))), [tasks]);
- const tasksV2 = useTasksV2();
- const hasTaskItems = tasksV2 !== undefined && tasksV2.length > 0;
- const escShortcut = useShortcutDisplay('chat:cancel', 'Chat', 'esc').toLowerCase();
- const todosShortcut = useShortcutDisplay('app:toggleTodos', 'Global', 'ctrl+t');
- const killAgentsShortcut = useShortcutDisplay('chat:killAgents', 'Chat', 'ctrl+x ctrl+k');
- const voiceKeyShortcut = feature('VOICE_MODE') ?
- // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- useShortcutDisplay('voice:pushToTalk', 'Chat', 'Space') : '';
+ const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false
+ const voiceState = feature('VOICE_MODE')
+ ? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+ useVoiceState(s => s.voiceState)
+ : ('idle' as const)
+ const voiceWarmingUp = feature('VOICE_MODE')
+ ? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+ useVoiceState(s => s.voiceWarmingUp)
+ : false
+ const hasSelection = useHasSelection()
+ const selGetState = useSelection().getState
+ const hasNextTick = nextTickAt !== null
+ const isCoordinator = feature('COORDINATOR_MODE')
+ ? coordinatorModule?.isCoordinatorMode() === true
+ : false
+ const runningTaskCount = useMemo(
+ () =>
+ count(
+ Object.values(tasks),
+ t =>
+ isBackgroundTask(t) &&
+ !(process.env.USER_TYPE === 'ant' && isPanelAgentTask(t)),
+ ),
+ [tasks],
+ )
+ const tasksV2 = useTasksV2()
+ const hasTaskItems = tasksV2 !== undefined && tasksV2.length > 0
+ const escShortcut = useShortcutDisplay(
+ 'chat:cancel',
+ 'Chat',
+ 'esc',
+ ).toLowerCase()
+ const todosShortcut = useShortcutDisplay(
+ 'app:toggleTodos',
+ 'Global',
+ 'ctrl+t',
+ )
+ const killAgentsShortcut = useShortcutDisplay(
+ 'chat:killAgents',
+ 'Chat',
+ 'ctrl+x ctrl+k',
+ )
+ const voiceKeyShortcut = feature('VOICE_MODE')
+ ? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+ useShortcutDisplay('voice:pushToTalk', 'Chat', 'Space')
+ : ''
// Captured at mount so the hint doesn't flicker mid-session if another
// CC instance increments the counter. Incremented once via useEffect the
// first time voice is enabled in this session — approximates "hint was
// shown" without tracking the exact render-time condition (which depends
// on parts/hintParts computed after the early-return hooks boundary).
- const [voiceHintUnderCap] = feature('VOICE_MODE') ?
+ const [voiceHintUnderCap] = feature('VOICE_MODE')
+ ? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+ useState(
+ () =>
+ (getGlobalConfig().voiceFooterHintSeenCount ?? 0) <
+ MAX_VOICE_HINT_SHOWS,
+ )
+ : [false]
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- useState(() => (getGlobalConfig().voiceFooterHintSeenCount ?? 0) < MAX_VOICE_HINT_SHOWS) : [false];
- // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- const voiceHintIncrementedRef = feature('VOICE_MODE') ? useRef(false) : null;
+ const voiceHintIncrementedRef = feature('VOICE_MODE') ? useRef(false) : null
useEffect(() => {
if (feature('VOICE_MODE')) {
- if (!voiceEnabled || !voiceHintUnderCap) return;
- if (voiceHintIncrementedRef?.current) return;
- if (voiceHintIncrementedRef) voiceHintIncrementedRef.current = true;
- const newCount = (getGlobalConfig().voiceFooterHintSeenCount ?? 0) + 1;
+ if (!voiceEnabled || !voiceHintUnderCap) return
+ if (voiceHintIncrementedRef?.current) return
+ if (voiceHintIncrementedRef) voiceHintIncrementedRef.current = true
+ const newCount = (getGlobalConfig().voiceFooterHintSeenCount ?? 0) + 1
saveGlobalConfig(prev => {
- if ((prev.voiceFooterHintSeenCount ?? 0) >= newCount) return prev;
- return {
- ...prev,
- voiceFooterHintSeenCount: newCount
- };
- });
+ if ((prev.voiceFooterHintSeenCount ?? 0) >= newCount) return prev
+ return { ...prev, voiceFooterHintSeenCount: newCount }
+ })
}
- }, [voiceEnabled, voiceHintUnderCap]);
- const isKillAgentsConfirmShowing = useAppState(s_7 => s_7.notifications.current?.key === 'kill-agents-confirm');
+ }, [voiceEnabled, voiceHintUnderCap])
+ const isKillAgentsConfirmShowing = useAppState(
+ s => s.notifications.current?.key === 'kill-agents-confirm',
+ )
// Derive team info from teamContext (no filesystem I/O needed)
// Match the same logic as TeamStatus to avoid trailing separator
// In-process mode uses Shift+Down/Up navigation, not footer teams menu
- const hasTeams = isAgentSwarmsEnabled() && !isInProcessEnabled() && teamContext !== undefined && count(Object.values(teamContext.teammates), t_0 => t_0.name !== 'team-lead') > 0;
+ const hasTeams =
+ isAgentSwarmsEnabled() &&
+ !isInProcessEnabled() &&
+ teamContext !== undefined &&
+ count(Object.values(teamContext.teammates), t => t.name !== 'team-lead') > 0
+
if (mode === 'bash') {
- return ! for bash mode;
+ return ! for bash mode
}
- const currentMode = toolPermissionContext?.mode;
- const hasActiveMode = !isDefaultMode(currentMode);
- const viewedTask = viewingAgentTaskId ? tasks[viewingAgentTaskId] : undefined;
- const isViewingTeammate = viewSelectionMode === 'viewing-agent' && viewedTask?.type === 'in_process_teammate';
- const isViewingCompletedTeammate = isViewingTeammate && viewedTask != null && viewedTask.status !== 'running';
- const hasBackgroundTasks = runningTaskCount > 0 || isViewingTeammate;
+
+ const currentMode = toolPermissionContext?.mode
+ const hasActiveMode = !isDefaultMode(currentMode)
+ const viewedTask = viewingAgentTaskId ? tasks[viewingAgentTaskId] : undefined
+ const isViewingTeammate =
+ viewSelectionMode === 'viewing-agent' &&
+ viewedTask?.type === 'in_process_teammate'
+ const isViewingCompletedTeammate =
+ isViewingTeammate && viewedTask != null && viewedTask.status !== 'running'
+ const hasBackgroundTasks = runningTaskCount > 0 || isViewingTeammate
// Count primary items (permission mode or coordinator mode, background tasks, and teams)
- const primaryItemCount = (isCoordinator || hasActiveMode ? 1 : 0) + (hasBackgroundTasks ? 1 : 0) + (hasTeams ? 1 : 0);
+ const primaryItemCount =
+ (isCoordinator || hasActiveMode ? 1 : 0) +
+ (hasBackgroundTasks ? 1 : 0) +
+ (hasTeams ? 1 : 0)
// PR indicator is short (~10 chars) — unlike the old diff indicator the
// >=100 threshold was tuned for. Now that auto mode is effectively the
// baseline, primaryItemCount is ≥1 for most sessions; keep the threshold
// low enough to show PR status on standard 80-col terminals.
- const shouldShowPrStatus = isPrStatusEnabled() && prStatus.number !== null && prStatus.reviewState !== null && prStatus.url !== null && primaryItemCount < 2 && (primaryItemCount === 0 || columns >= 80);
+ const shouldShowPrStatus =
+ isPrStatusEnabled() &&
+ prStatus.number !== null &&
+ prStatus.reviewState !== null &&
+ prStatus.url !== null &&
+ primaryItemCount < 2 &&
+ (primaryItemCount === 0 || columns >= 80)
// Hide the shift+tab hint when there are 2 primary items
- const shouldShowModeHint = primaryItemCount < 2;
+ const shouldShowModeHint = primaryItemCount < 2
// Check if we have in-process teammates (showing pills)
// In spinner-tree mode, pills are disabled - teammates appear in the spinner tree instead
- const hasInProcessTeammates = !showSpinnerTree && hasBackgroundTasks && Object.values(tasks).some(t_1 => t_1.type === 'in_process_teammate');
- const hasTeammatePills = hasInProcessTeammates || !showSpinnerTree && isViewingTeammate;
+ const hasInProcessTeammates =
+ !showSpinnerTree &&
+ hasBackgroundTasks &&
+ Object.values(tasks).some(t => t.type === 'in_process_teammate')
+ const hasTeammatePills =
+ hasInProcessTeammates || (!showSpinnerTree && isViewingTeammate)
// In remote mode (`claude assistant`, --teleport) the agent runs elsewhere;
// the local permission mode shown here doesn't reflect the agent's state.
// Rendered before the tasks pill so a long pill label (e.g. ultraplan URL)
// doesn't push the mode indicator off-screen.
- const modePart = currentMode && hasActiveMode && !getIsRemoteMode() ?
+ const modePart =
+ currentMode && hasActiveMode && !getIsRemoteMode() ? (
+
{permissionModeSymbol(currentMode)}{' '}
{permissionModeTitle(currentMode).toLowerCase()} on
- {shouldShowModeHint &&
+ {shouldShowModeHint && (
+
{' '}
-
- }
- : null;
+
+
+ )}
+
+ ) : null
// Build parts array - exclude BackgroundTaskStatus when we have teammate pills
// (teammate pills get their own row)
const parts = [
- // Remote session indicator
- ...(remoteSessionUrl ? [
+ // Remote session indicator
+ ...(remoteSessionUrl
+ ? [
+
{figures.circleDouble} remote
- ] : []),
- // BackgroundTaskStatus is NOT in parts — it renders as a Box sibling so
- // its click-target Box isn't nested inside the
- // wrapper (reconciler throws on Box-in-Text).
- // Tmux pill (ant-only) — appears right after tasks in nav order
- ...(process.env.USER_TYPE === 'ant' && hasTmuxSession && typeof TungstenPill === 'function' ? [] : []), ...(isAgentSwarmsEnabled() && hasTeams ? [] : []), ...(shouldShowPrStatus ? [] : [])];
+ ,
+ ]
+ : []),
+ // BackgroundTaskStatus is NOT in parts — it renders as a Box sibling so
+ // its click-target Box isn't nested inside the
+ // wrapper (reconciler throws on Box-in-Text).
+ // Tmux pill (ant-only) — appears right after tasks in nav order
+ ...(process.env.USER_TYPE === 'ant' && hasTmuxSession
+ ? []
+ : []),
+ ...(isAgentSwarmsEnabled() && hasTeams
+ ? [
+ ,
+ ]
+ : []),
+ ...(shouldShowPrStatus
+ ? [
+ ,
+ ]
+ : []),
+ ]
// Check if any in-process teammates exist (for hint text cycling)
- const hasAnyInProcessTeammates = Object.values(tasks).some(t_2 => t_2.type === 'in_process_teammate' && t_2.status === 'running');
- const hasRunningAgentTasks = Object.values(tasks).some(t_3 => t_3.type === 'local_agent' && t_3.status === 'running');
+ const hasAnyInProcessTeammates = Object.values(tasks).some(
+ t => t.type === 'in_process_teammate' && t.status === 'running',
+ )
+ const hasRunningAgentTasks = Object.values(tasks).some(
+ t => t.type === 'local_agent' && t.status === 'running',
+ )
// Get hint parts separately for potential second-line rendering
- const hintParts = showHint ? getSpinnerHintParts(isLoading, escShortcut, todosShortcut, killAgentsShortcut, hasTaskItems, expandedView, hasAnyInProcessTeammates, hasRunningAgentTasks, isKillAgentsConfirmShowing) : [];
+ const hintParts = showHint
+ ? getSpinnerHintParts(
+ isLoading,
+ escShortcut,
+ todosShortcut,
+ killAgentsShortcut,
+ hasTaskItems,
+ expandedView,
+ hasAnyInProcessTeammates,
+ hasRunningAgentTasks,
+ isKillAgentsConfirmShowing,
+ )
+ : []
+
if (isViewingCompletedTeammate) {
- parts.push(
-
- );
+ parts.push(
+
+
+ ,
+ )
} else if ((feature('PROACTIVE') || feature('KAIROS')) && hasNextTick) {
- parts.push();
+ parts.push()
} else if (!hasTeammatePills && showHint) {
- parts.push(...hintParts);
+ parts.push(...hintParts)
}
// When we have teammate pills, always render them on their own line above other parts
if (hasTeammatePills) {
// Don't append spinner hints when viewing a completed teammate —
// the "esc to return to team lead" hint already replaces "esc to interrupt"
- const otherParts = [...(modePart ? [modePart] : []), ...parts, ...(isViewingCompletedTeammate ? [] : hintParts)];
- return
+ const otherParts = [
+ ...(modePart ? [modePart] : []),
+ ...parts,
+ ...(isViewingCompletedTeammate ? [] : hintParts),
+ ]
+ return (
+
-
+
- {otherParts.length > 0 &&
+ {otherParts.length > 0 && (
+
{otherParts}
- }
- ;
+
+ )}
+
+ )
}
// Add "↓ to manage tasks" hint when panel has visible rows
- const hasCoordinatorTasks = (process.env.USER_TYPE) === 'ant' && getVisibleAgentTasks(tasks).length > 0;
+ const hasCoordinatorTasks =
+ process.env.USER_TYPE === 'ant' && getVisibleAgentTasks(tasks).length > 0
// Tasks pill renders as a Box sibling (not a parts entry) so its
// click-target Box isn't nested inside — the
// reconciler throws on Box-in-Text. Computed here so the empty-checks
// below still treat "pill present" as non-empty.
- const tasksPart = hasBackgroundTasks && !hasTeammatePills && !shouldHideTasksFooter(tasks, showSpinnerTree) ? : null;
+ const tasksPart =
+ hasBackgroundTasks &&
+ !hasTeammatePills &&
+ !shouldHideTasksFooter(tasks, showSpinnerTree) ? (
+
+ ) : null
+
if (parts.length === 0 && !tasksPart && !modePart && showHint) {
- parts.push(
+ parts.push(
+
? for shortcuts
- );
+ ,
+ )
}
// Only replace the idle voice hint when there's something to say — otherwise
// fall through instead of showing an empty Byline. "esc to clear" was removed
// (looked like "esc to interrupt" when idle; esc-clears-selection is standard
// UX) leaving only ctrl+c (copyOnSelect off) and the xterm.js native-select hint.
- const copyOnSelect = getGlobalConfig().copyOnSelect ?? true;
- const selectionHintHasContent = hasSelection && (!copyOnSelect || isXtermJs());
+ const copyOnSelect = getGlobalConfig().copyOnSelect ?? true
+ const selectionHintHasContent = hasSelection && (!copyOnSelect || isXtermJs())
// Warmup hint takes priority — when the user is actively holding
// the activation key, show feedback regardless of other hints.
if (feature('VOICE_MODE') && voiceEnabled && voiceWarmingUp) {
- parts.push();
+ parts.push()
} else if (isFullscreenEnvEnabled() && selectionHintHasContent) {
// xterm.js (VS Code/Cursor/Windsurf) force-selection modifier is
// platform-specific and gated on macOS (SelectionService.shouldForceSelection):
@@ -434,23 +550,52 @@ function ModeIndicator({
// option+click hint they just tried.
// Non-reactive getState() read is safe: lastPressHadAlt is immutable
// while hasSelection is true (set pre-drag, cleared with selection).
- const isMac = getPlatform() === 'macos';
- const altClickFailed = isMac && (selGetState()?.lastPressHadAlt ?? false);
- parts.push(
+ const isMac = getPlatform() === 'macos'
+ const altClickFailed = isMac && (selGetState()?.lastPressHadAlt ?? false)
+ parts.push(
+
- {!copyOnSelect && }
- {isXtermJs() && (altClickFailed ? set macOptionClickForcesSelection in VS Code settings : )}
+ {!copyOnSelect && (
+
+ )}
+ {isXtermJs() &&
+ (altClickFailed ? (
+ set macOptionClickForcesSelection in VS Code settings
+ ) : (
+
+ ))}
- );
- } else if (feature('VOICE_MODE') && parts.length > 0 && showHint && voiceEnabled && voiceState === 'idle' && hintParts.length === 0 && voiceHintUnderCap) {
- parts.push(
+ ,
+ )
+ } else if (
+ feature('VOICE_MODE') &&
+ parts.length > 0 &&
+ showHint &&
+ voiceEnabled &&
+ voiceState === 'idle' &&
+ hintParts.length === 0 &&
+ voiceHintUnderCap
+ ) {
+ parts.push(
+
hold {voiceKeyShortcut} to speak
- );
+ ,
+ )
}
+
if ((tasksPart || hasCoordinatorTasks) && showHint && !hasTeams) {
- parts.push(
- {tasksSelected ? : }
- );
+ parts.push(
+
+ {tasksSelected ? (
+
+ ) : (
+
+ )}
+ ,
+ )
}
// In fullscreen the bottom section is flexShrink:0 — every row here
@@ -462,55 +607,98 @@ function ModeIndicator({
// from 0→1 row. Always render 1 row in fullscreen; return a space when
// empty so Yoga reserves the row without painting anything visible.
if (parts.length === 0 && !tasksPart && !modePart) {
- return isFullscreenEnvEnabled() ? : null;
+ return isFullscreenEnvEnabled() ? : null
}
// flexShrink=0 keeps mode + pill at natural width; the remaining parts
// truncate at the tail as one string inside the Text wrapper.
- return
- {modePart &&
+ return (
+
+ {modePart && (
+
{modePart}
{(tasksPart || parts.length > 0) && · }
- }
- {tasksPart &&
+
+ )}
+ {tasksPart && (
+
{tasksPart}
{parts.length > 0 && · }
- }
- {parts.length > 0 &&
+
+ )}
+ {parts.length > 0 && (
+
{parts}
- }
- ;
+
+ )}
+
+ )
}
-function getSpinnerHintParts(isLoading: boolean, escShortcut: string, todosShortcut: string, killAgentsShortcut: string, hasTaskItems: boolean, expandedView: 'none' | 'tasks' | 'teammates', hasTeammates: boolean, hasRunningAgentTasks: boolean, isKillAgentsConfirmShowing: boolean): React.ReactElement[] {
- let toggleAction: string;
+
+function getSpinnerHintParts(
+ isLoading: boolean,
+ escShortcut: string,
+ todosShortcut: string,
+ killAgentsShortcut: string,
+ hasTaskItems: boolean,
+ expandedView: 'none' | 'tasks' | 'teammates',
+ hasTeammates: boolean,
+ hasRunningAgentTasks: boolean,
+ isKillAgentsConfirmShowing: boolean,
+): React.ReactElement[] {
+ let toggleAction: string
if (hasTeammates) {
// Cycling: none → tasks → teammates → none
switch (expandedView) {
case 'none':
- toggleAction = 'show tasks';
- break;
+ toggleAction = 'show tasks'
+ break
case 'tasks':
- toggleAction = 'show teammates';
- break;
+ toggleAction = 'show teammates'
+ break
case 'teammates':
- toggleAction = 'hide';
- break;
+ toggleAction = 'hide'
+ break
}
} else {
- toggleAction = expandedView === 'tasks' ? 'hide tasks' : 'show tasks';
+ toggleAction = expandedView === 'tasks' ? 'hide tasks' : 'show tasks'
}
// Show the toggle hint only when there are task items to display or
// teammates to cycle to
- const showToggleHint = hasTaskItems || hasTeammates;
- return [...(isLoading ? [
+ const showToggleHint = hasTaskItems || hasTeammates
+
+ return [
+ ...(isLoading
+ ? [
+
- ] : []), ...(!isLoading && hasRunningAgentTasks && !isKillAgentsConfirmShowing ? [
-
- ] : []), ...(showToggleHint ? [
-
- ] : [])];
+ ,
+ ]
+ : []),
+ ...(!isLoading && hasRunningAgentTasks && !isKillAgentsConfirmShowing
+ ? [
+
+
+ ,
+ ]
+ : []),
+ ...(showToggleHint
+ ? [
+
+
+ ,
+ ]
+ : []),
+ ]
}
+
function isPrStatusEnabled(): boolean {
- return getGlobalConfig().prStatusFooterEnabled ?? true;
+ return getGlobalConfig().prStatusFooterEnabled ?? true
}
diff --git a/src/components/PromptInput/PromptInputFooterSuggestions.tsx b/src/components/PromptInput/PromptInputFooterSuggestions.tsx
index 5f1fa74a9..0728e5255 100644
--- a/src/components/PromptInput/PromptInputFooterSuggestions.tsx
+++ b/src/components/PromptInput/PromptInputFooterSuggestions.tsx
@@ -1,292 +1,248 @@
-import { c as _c } from "react/compiler-runtime";
-import * as React from 'react';
-import { memo, type ReactNode } from 'react';
-import { useTerminalSize } from '../../hooks/useTerminalSize.js';
-import { stringWidth } from '../../ink/stringWidth.js';
-import { Box, Text } from '../../ink.js';
-import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js';
-import type { Theme } from '../../utils/theme.js';
+import * as React from 'react'
+import { memo, type ReactNode } from 'react'
+import { useTerminalSize } from '../../hooks/useTerminalSize.js'
+import { stringWidth } from '../../ink/stringWidth.js'
+import { Box, Text } from '../../ink.js'
+import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js'
+import type { Theme } from '../../utils/theme.js'
+
export type SuggestionItem = {
- id: string;
- displayText: string;
- tag?: string;
- description?: string;
- metadata?: unknown;
- color?: keyof Theme;
-};
-export type SuggestionType = 'command' | 'file' | 'directory' | 'agent' | 'shell' | 'custom-title' | 'slack-channel' | 'none';
-export const OVERLAY_MAX_ITEMS = 5;
+ id: string
+ displayText: string
+ tag?: string
+ description?: string
+ metadata?: unknown
+ color?: keyof Theme
+}
+
+export type SuggestionType =
+ | 'command'
+ | 'file'
+ | 'directory'
+ | 'agent'
+ | 'shell'
+ | 'custom-title'
+ | 'slack-channel'
+ | 'none'
+
+export const OVERLAY_MAX_ITEMS = 5
/**
* Get the icon for a suggestion based on its type
* Icons: + for files, ◇ for MCP resources, * for agents
*/
function getIcon(itemId: string): string {
- if (itemId.startsWith('file-')) return '+';
- if (itemId.startsWith('mcp-resource-')) return '◇';
- if (itemId.startsWith('agent-')) return '*';
- return '+';
+ if (itemId.startsWith('file-')) return '+'
+ if (itemId.startsWith('mcp-resource-')) return '◇'
+ if (itemId.startsWith('agent-')) return '*'
+ return '+'
}
/**
* Check if an item is a unified suggestion type (file, mcp-resource, or agent)
*/
function isUnifiedSuggestion(itemId: string): boolean {
- return itemId.startsWith('file-') || itemId.startsWith('mcp-resource-') || itemId.startsWith('agent-');
+ return (
+ itemId.startsWith('file-') ||
+ itemId.startsWith('mcp-resource-') ||
+ itemId.startsWith('agent-')
+ )
}
-const SuggestionItemRow = memo(function SuggestionItemRow(t0: { item: SuggestionItem; maxColumnWidth: number; isSelected: boolean }) {
- const $ = _c(36);
- const {
- item,
- maxColumnWidth,
- isSelected
- } = t0;
- const columns = useTerminalSize().columns;
- const isUnified = isUnifiedSuggestion(item.id);
+
+const SuggestionItemRow = memo(function SuggestionItemRow({
+ item,
+ maxColumnWidth,
+ isSelected,
+}: {
+ item: SuggestionItem
+ maxColumnWidth?: number
+ isSelected: boolean
+}): ReactNode {
+ const columns = useTerminalSize().columns
+ const isUnified = isUnifiedSuggestion(item.id)
+
+ // For unified suggestions (file, mcp-resource, agent), use single-line layout with icon
if (isUnified) {
- let t1;
- if ($[0] !== item.id) {
- t1 = getIcon(item.id);
- $[0] = item.id;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- const icon = t1;
- const textColor = isSelected ? "suggestion" : undefined;
- const dimColor = !isSelected;
- const isFile = item.id.startsWith("file-");
- const isMcpResource = item.id.startsWith("mcp-resource-");
- const separatorWidth = item.description ? 3 : 0;
- let displayText;
+ const icon = getIcon(item.id)
+ const textColor: keyof Theme | undefined = isSelected
+ ? 'suggestion'
+ : undefined
+ const dimColor = !isSelected
+
+ const isFile = item.id.startsWith('file-')
+ const isMcpResource = item.id.startsWith('mcp-resource-')
+
+ // Calculate layout widths
+ // Layout: "X " (2) + displayText + " – " (3) + description + padding (4)
+ const iconWidth = 2 // icon + space (fixed)
+ const paddingWidth = 4
+ const separatorWidth = item.description ? 3 : 0 // ' – ' separator
+
+ // For files, truncate middle of path to show both directory context and filename
+ // For MCP resources, limit displayText to 30 chars (truncate from end)
+ // For agents, no truncation
+ let displayText: string
if (isFile) {
- let t2;
- if ($[2] !== item.description) {
- t2 = item.description ? Math.min(20, stringWidth(item.description)) : 0;
- $[2] = item.description;
- $[3] = t2;
- } else {
- t2 = $[3];
- }
- const descReserve = t2;
- const maxPathLength = columns - 2 - 4 - separatorWidth - descReserve;
- let t3;
- if ($[4] !== item.displayText || $[5] !== maxPathLength) {
- t3 = truncatePathMiddle(item.displayText, maxPathLength);
- $[4] = item.displayText;
- $[5] = maxPathLength;
- $[6] = t3;
- } else {
- t3 = $[6];
- }
- displayText = t3;
+ // Reserve space for description if present, otherwise use all available space
+ const descReserve = item.description
+ ? Math.min(20, stringWidth(item.description))
+ : 0
+ const maxPathLength =
+ columns - iconWidth - paddingWidth - separatorWidth - descReserve
+ displayText = truncatePathMiddle(item.displayText, maxPathLength)
+ } else if (isMcpResource) {
+ const maxDisplayTextLength = 30
+ displayText = truncateToWidth(item.displayText, maxDisplayTextLength)
} else {
- if (isMcpResource) {
- let t2;
- if ($[7] !== item.displayText) {
- t2 = truncateToWidth(item.displayText, 30);
- $[7] = item.displayText;
- $[8] = t2;
- } else {
- t2 = $[8];
- }
- displayText = t2;
- } else {
- displayText = item.displayText;
- }
+ displayText = item.displayText
}
- const availableWidth = columns - 2 - stringWidth(displayText) - separatorWidth - 4;
- let lineContent;
+
+ const availableWidth =
+ columns -
+ iconWidth -
+ stringWidth(displayText) -
+ separatorWidth -
+ paddingWidth
+
+ // Build the full line as a single string to prevent wrapping
+ let lineContent: string
if (item.description) {
- const maxDescLength = Math.max(0, availableWidth);
- let t2;
- if ($[9] !== item.description || $[10] !== maxDescLength) {
- t2 = truncateToWidth(item.description.replace(/\s+/g, " "), maxDescLength);
- $[9] = item.description;
- $[10] = maxDescLength;
- $[11] = t2;
- } else {
- t2 = $[11];
- }
- const truncatedDesc = t2;
- lineContent = `${icon} ${displayText} – ${truncatedDesc}`;
+ const maxDescLength = Math.max(0, availableWidth)
+ const truncatedDesc = truncateToWidth(
+ item.description.replace(/\s+/g, ' '),
+ maxDescLength,
+ )
+ lineContent = `${icon} ${displayText} – ${truncatedDesc}`
} else {
- lineContent = `${icon} ${displayText}`;
+ lineContent = `${icon} ${displayText}`
}
- let t2;
- if ($[12] !== dimColor || $[13] !== lineContent || $[14] !== textColor) {
- t2 = {lineContent};
- $[12] = dimColor;
- $[13] = lineContent;
- $[14] = textColor;
- $[15] = t2;
- } else {
- t2 = $[15];
- }
- return t2;
+
+ return (
+
+ {lineContent}
+
+ )
}
- const maxNameWidth = Math.floor(columns * 0.4);
- const displayTextWidth = Math.min(maxColumnWidth ?? stringWidth(item.displayText) + 5, maxNameWidth);
- const textColor_0 = item.color || (isSelected ? "suggestion" : undefined);
- const shouldDim = !isSelected;
- let displayText_0 = item.displayText;
- if (stringWidth(displayText_0) > displayTextWidth - 2) {
- const t1 = displayTextWidth - 2;
- let t2;
- if ($[16] !== displayText_0 || $[17] !== t1) {
- t2 = truncateToWidth(displayText_0, t1);
- $[16] = displayText_0;
- $[17] = t1;
- $[18] = t2;
- } else {
- t2 = $[18];
- }
- displayText_0 = t2;
+
+ // For non-unified suggestions (commands, shell, etc.), use improved layout from main
+ // Cap the command name column at 40% of terminal width to ensure description has space
+ const maxNameWidth = Math.floor(columns * 0.4)
+ const displayTextWidth = Math.min(
+ maxColumnWidth ?? stringWidth(item.displayText) + 5,
+ maxNameWidth,
+ )
+
+ const textColor = item.color || (isSelected ? 'suggestion' : undefined)
+ const shouldDim = !isSelected
+
+ // Truncate and pad the display text to fixed width
+ let displayText = item.displayText
+ if (stringWidth(displayText) > displayTextWidth - 2) {
+ displayText = truncateToWidth(displayText, displayTextWidth - 2)
}
- const paddedDisplayText = displayText_0 + " ".repeat(Math.max(0, displayTextWidth - stringWidth(displayText_0)));
- const tagText = item.tag ? `[${item.tag}] ` : "";
- const tagWidth = stringWidth(tagText);
- const descriptionWidth = Math.max(0, columns - displayTextWidth - tagWidth - 4);
- let t1;
- if ($[19] !== descriptionWidth || $[20] !== item.description) {
- t1 = item.description ? truncateToWidth(item.description.replace(/\s+/g, " "), descriptionWidth) : "";
- $[19] = descriptionWidth;
- $[20] = item.description;
- $[21] = t1;
- } else {
- t1 = $[21];
- }
- const truncatedDescription = t1;
- let t2;
- if ($[22] !== paddedDisplayText || $[23] !== shouldDim || $[24] !== textColor_0) {
- t2 = {paddedDisplayText};
- $[22] = paddedDisplayText;
- $[23] = shouldDim;
- $[24] = textColor_0;
- $[25] = t2;
- } else {
- t2 = $[25];
- }
- let t3;
- if ($[26] !== tagText) {
- t3 = tagText ? {tagText} : null;
- $[26] = tagText;
- $[27] = t3;
- } else {
- t3 = $[27];
- }
- const t4 = isSelected ? "suggestion" : undefined;
- const t5 = !isSelected;
- let t6;
- if ($[28] !== t4 || $[29] !== t5 || $[30] !== truncatedDescription) {
- t6 = {truncatedDescription};
- $[28] = t4;
- $[29] = t5;
- $[30] = truncatedDescription;
- $[31] = t6;
- } else {
- t6 = $[31];
- }
- let t7;
- if ($[32] !== t2 || $[33] !== t3 || $[34] !== t6) {
- t7 = {t2}{t3}{t6};
- $[32] = t2;
- $[33] = t3;
- $[34] = t6;
- $[35] = t7;
- } else {
- t7 = $[35];
- }
- return t7;
-});
+ const paddedDisplayText =
+ displayText +
+ ' '.repeat(Math.max(0, displayTextWidth - stringWidth(displayText)))
+
+ const tagText = item.tag ? `[${item.tag}] ` : ''
+ const tagWidth = stringWidth(tagText)
+ const descriptionWidth = Math.max(
+ 0,
+ columns - displayTextWidth - tagWidth - 4,
+ )
+ // Skill descriptions can contain newlines (e.g. /claude-api's "TRIGGER
+ // when:" block). A multi-line row grows the overlay past minHeight; when
+ // the filter narrows past that skill, the overlay shrinks and leaves
+ // ghost rows. Flatten to one line before truncating.
+ const truncatedDescription = item.description
+ ? truncateToWidth(item.description.replace(/\s+/g, ' '), descriptionWidth)
+ : ''
+
+ return (
+
+
+ {paddedDisplayText}
+
+ {tagText ? {tagText} : null}
+
+ {truncatedDescription}
+
+
+ )
+})
+
type Props = {
- suggestions: SuggestionItem[];
- selectedSuggestion: number;
- maxColumnWidth?: number;
+ suggestions: SuggestionItem[]
+ selectedSuggestion: number
+ maxColumnWidth?: number
/**
* When true, the suggestions are rendered inside a position=absolute
* overlay. We omit minHeight and flex-end so the y-clamp in the
* renderer doesn't push fewer items down into the prompt area.
*/
- overlay?: boolean;
-};
-export function PromptInputFooterSuggestions(t0) {
- const $ = _c(22);
- const {
- suggestions,
- selectedSuggestion,
- maxColumnWidth: maxColumnWidthProp,
- overlay
- } = t0;
- const {
- rows
- } = useTerminalSize();
- const maxVisibleItems = overlay ? OVERLAY_MAX_ITEMS : Math.min(6, Math.max(1, rows - 3));
+ overlay?: boolean
+}
+
+export function PromptInputFooterSuggestions({
+ suggestions,
+ selectedSuggestion,
+ maxColumnWidth: maxColumnWidthProp,
+ overlay,
+}: Props): ReactNode {
+ const { rows } = useTerminalSize()
+ // Maximum number of suggestions to show at once (leaving space for prompt).
+ // Overlay mode (fullscreen) uses a fixed 5 — the floating box sits over
+ // the ScrollBox, so terminal height isn't the constraint.
+ const maxVisibleItems = overlay
+ ? OVERLAY_MAX_ITEMS
+ : Math.min(6, Math.max(1, rows - 3))
+
+ // No suggestions to display
if (suggestions.length === 0) {
- return null;
+ return null
}
- let t1;
- if ($[0] !== maxColumnWidthProp || $[1] !== suggestions) {
- t1 = maxColumnWidthProp ?? Math.max(...suggestions.map(_temp)) + 5;
- $[0] = maxColumnWidthProp;
- $[1] = suggestions;
- $[2] = t1;
- } else {
- t1 = $[2];
- }
- const maxColumnWidth = t1;
- const startIndex = Math.max(0, Math.min(selectedSuggestion - Math.floor(maxVisibleItems / 2), suggestions.length - maxVisibleItems));
- const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length);
- let T0;
- let t2;
- let t3;
- let t4;
- if ($[3] !== endIndex || $[4] !== maxColumnWidth || $[5] !== overlay || $[6] !== selectedSuggestion || $[7] !== startIndex || $[8] !== suggestions) {
- const visibleItems = suggestions.slice(startIndex, endIndex);
- T0 = Box;
- t2 = "column";
- t3 = overlay ? undefined : "flex-end";
- let t5;
- if ($[13] !== maxColumnWidth || $[14] !== selectedSuggestion || $[15] !== suggestions) {
- t5 = item_0 => ;
- $[13] = maxColumnWidth;
- $[14] = selectedSuggestion;
- $[15] = suggestions;
- $[16] = t5;
- } else {
- t5 = $[16];
- }
- t4 = visibleItems.map(t5);
- $[3] = endIndex;
- $[4] = maxColumnWidth;
- $[5] = overlay;
- $[6] = selectedSuggestion;
- $[7] = startIndex;
- $[8] = suggestions;
- $[9] = T0;
- $[10] = t2;
- $[11] = t3;
- $[12] = t4;
- } else {
- T0 = $[9];
- t2 = $[10];
- t3 = $[11];
- t4 = $[12];
- }
- let t5;
- if ($[17] !== T0 || $[18] !== t2 || $[19] !== t3 || $[20] !== t4) {
- t5 = {t4};
- $[17] = T0;
- $[18] = t2;
- $[19] = t3;
- $[20] = t4;
- $[21] = t5;
- } else {
- t5 = $[21];
- }
- return t5;
+
+ // Use prop if provided (stable width from all commands), otherwise calculate from visible
+ const maxColumnWidth =
+ maxColumnWidthProp ??
+ Math.max(...suggestions.map(item => stringWidth(item.displayText))) + 5
+
+ // Calculate visible items range based on selected index
+ const startIndex = Math.max(
+ 0,
+ Math.min(
+ selectedSuggestion - Math.floor(maxVisibleItems / 2),
+ suggestions.length - maxVisibleItems,
+ ),
+ )
+ const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length)
+ const visibleItems = suggestions.slice(startIndex, endIndex)
+
+ // In non-overlay (inline) mode, justifyContent keeps suggestions
+ // anchored to the bottom (near the prompt). In overlay mode we omit
+ // both minHeight and flex-end: the parent is position=absolute with
+ // bottom='100%', so its y is clamped to 0 by the renderer when it
+ // would go negative. Adding minHeight + flex-end would create empty
+ // padding rows that shift the visible items down into the prompt area
+ // when the list has fewer items than maxVisibleItems.
+ return (
+
+ {visibleItems.map(item => (
+
+ ))}
+
+ )
}
-function _temp(item) {
- return stringWidth(item.displayText);
-}
-export default memo(PromptInputFooterSuggestions);
+
+export default memo(PromptInputFooterSuggestions)
diff --git a/src/components/PromptInput/PromptInputHelpMenu.tsx b/src/components/PromptInput/PromptInputHelpMenu.tsx
index f8e275765..5f15327d2 100644
--- a/src/components/PromptInput/PromptInputHelpMenu.tsx
+++ b/src/components/PromptInput/PromptInputHelpMenu.tsx
@@ -1,357 +1,149 @@
-import { c as _c } from "react/compiler-runtime";
-import { feature } from 'bun:bundle';
-import * as React from 'react';
-import { Box, Text } from 'src/ink.js';
-import { getPlatform } from 'src/utils/platform.js';
-import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js';
-import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
-import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js';
-import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js';
-import { getNewlineInstructions } from './utils.js';
+import { feature } from 'bun:bundle'
+import * as React from 'react'
+import { Box, Text } from 'src/ink.js'
+import { getPlatform } from 'src/utils/platform.js'
+import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js'
+import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
+import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
+import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js'
+import { getNewlineInstructions } from './utils.js'
/** Format a shortcut for display in the help menu (e.g., "ctrl+o" → "ctrl + o") */
function formatShortcut(shortcut: string): string {
- return shortcut.replace(/\+/g, ' + ');
+ return shortcut.replace(/\+/g, ' + ')
}
+
type Props = {
- dimColor?: boolean;
- fixedWidth?: boolean;
- gap?: number;
- paddingX?: number;
-};
-export function PromptInputHelpMenu(props) {
- const $ = _c(99);
- const {
- dimColor,
- fixedWidth,
- gap,
- paddingX
- } = props;
- const t0 = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
- let t1;
- if ($[0] !== t0) {
- t1 = formatShortcut(t0);
- $[0] = t0;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- const transcriptShortcut = t1;
- const t2 = useShortcutDisplay("app:toggleTodos", "Global", "ctrl+t");
- let t3;
- if ($[2] !== t2) {
- t3 = formatShortcut(t2);
- $[2] = t2;
- $[3] = t3;
- } else {
- t3 = $[3];
- }
- const todosShortcut = t3;
- const t4 = useShortcutDisplay("chat:undo", "Chat", "ctrl+_");
- let t5;
- if ($[4] !== t4) {
- t5 = formatShortcut(t4);
- $[4] = t4;
- $[5] = t5;
- } else {
- t5 = $[5];
- }
- const undoShortcut = t5;
- const t6 = useShortcutDisplay("chat:stash", "Chat", "ctrl+s");
- let t7;
- if ($[6] !== t6) {
- t7 = formatShortcut(t6);
- $[6] = t6;
- $[7] = t7;
- } else {
- t7 = $[7];
- }
- const stashShortcut = t7;
- const t8 = useShortcutDisplay("chat:cycleMode", "Chat", "shift+tab");
- let t9;
- if ($[8] !== t8) {
- t9 = formatShortcut(t8);
- $[8] = t8;
- $[9] = t9;
- } else {
- t9 = $[9];
- }
- const cycleModeShortcut = t9;
- const t10 = useShortcutDisplay("chat:modelPicker", "Chat", "alt+p");
- let t11;
- if ($[10] !== t10) {
- t11 = formatShortcut(t10);
- $[10] = t10;
- $[11] = t11;
- } else {
- t11 = $[11];
- }
- const modelPickerShortcut = t11;
- const t12 = useShortcutDisplay("chat:fastMode", "Chat", "alt+o");
- let t13;
- if ($[12] !== t12) {
- t13 = formatShortcut(t12);
- $[12] = t12;
- $[13] = t13;
- } else {
- t13 = $[13];
- }
- const fastModeShortcut = t13;
- const t14 = useShortcutDisplay("chat:externalEditor", "Chat", "ctrl+g");
- let t15;
- if ($[14] !== t14) {
- t15 = formatShortcut(t14);
- $[14] = t14;
- $[15] = t15;
- } else {
- t15 = $[15];
- }
- const externalEditorShortcut = t15;
- const t16 = useShortcutDisplay("app:toggleTerminal", "Global", "meta+j");
- let t17;
- if ($[16] !== t16) {
- t17 = formatShortcut(t16);
- $[16] = t16;
- $[17] = t17;
- } else {
- t17 = $[17];
- }
- const terminalShortcut = t17;
- const t18 = useShortcutDisplay("chat:imagePaste", "Chat", "ctrl+v");
- let t19;
- if ($[18] !== t18) {
- t19 = formatShortcut(t18);
- $[18] = t18;
- $[19] = t19;
- } else {
- t19 = $[19];
- }
- const imagePasteShortcut = t19;
- let t20;
- if ($[20] !== dimColor || $[21] !== terminalShortcut) {
- t20 = feature("TERMINAL_PANEL") ? getFeatureValue_CACHED_MAY_BE_STALE("tengu_terminal_panel", false) ? {terminalShortcut} for terminal : null : null;
- $[20] = dimColor;
- $[21] = terminalShortcut;
- $[22] = t20;
- } else {
- t20 = $[22];
- }
- const terminalShortcutElement = t20;
- const t21 = fixedWidth ? 24 : undefined;
- let t22;
- if ($[23] !== dimColor) {
- t22 = ! for bash mode;
- $[23] = dimColor;
- $[24] = t22;
- } else {
- t22 = $[24];
- }
- let t23;
- if ($[25] !== dimColor) {
- t23 = / for commands;
- $[25] = dimColor;
- $[26] = t23;
- } else {
- t23 = $[26];
- }
- let t24;
- if ($[27] !== dimColor) {
- t24 = @ for file paths;
- $[27] = dimColor;
- $[28] = t24;
- } else {
- t24 = $[28];
- }
- let t25;
- if ($[29] !== dimColor) {
- t25 = {"& for background"};
- $[29] = dimColor;
- $[30] = t25;
- } else {
- t25 = $[30];
- }
- let t26;
- if ($[31] !== dimColor) {
- t26 = /btw for side question;
- $[31] = dimColor;
- $[32] = t26;
- } else {
- t26 = $[32];
- }
- let t27;
- if ($[33] !== t21 || $[34] !== t22 || $[35] !== t23 || $[36] !== t24 || $[37] !== t25 || $[38] !== t26) {
- t27 = {t22}{t23}{t24}{t25}{t26};
- $[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 = double tap esc to clear input;
- $[40] = dimColor;
- $[41] = t29;
- } else {
- t29 = $[41];
- }
- let t30;
- if ($[42] !== cycleModeShortcut || $[43] !== dimColor) {
- t30 = {cycleModeShortcut}{" "}{false ? "to cycle modes" : "to auto-accept edits"};
- $[42] = cycleModeShortcut;
- $[43] = dimColor;
- $[44] = t30;
- } else {
- t30 = $[44];
- }
- let t31;
- if ($[45] !== dimColor || $[46] !== transcriptShortcut) {
- t31 = {transcriptShortcut} for verbose output;
- $[45] = dimColor;
- $[46] = transcriptShortcut;
- $[47] = t31;
- } else {
- t31 = $[47];
- }
- let t32;
- if ($[48] !== dimColor || $[49] !== todosShortcut) {
- t32 = {todosShortcut} to toggle tasks;
- $[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 = {t33};
- $[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 = {t29}{t30}{t31}{t32}{terminalShortcutElement}{t34};
- $[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 = {undoShortcut} to undo;
- $[62] = dimColor;
- $[63] = undoShortcut;
- $[64] = t36;
- } else {
- t36 = $[64];
- }
- let t37;
- if ($[65] !== dimColor) {
- t37 = getPlatform() !== "windows" && ctrl + z to suspend;
- $[65] = dimColor;
- $[66] = t37;
- } else {
- t37 = $[66];
- }
- let t38;
- if ($[67] !== dimColor || $[68] !== imagePasteShortcut) {
- t38 = {imagePasteShortcut} to paste images;
- $[67] = dimColor;
- $[68] = imagePasteShortcut;
- $[69] = t38;
- } else {
- t38 = $[69];
- }
- let t39;
- if ($[70] !== dimColor || $[71] !== modelPickerShortcut) {
- t39 = {modelPickerShortcut} to switch model;
- $[70] = dimColor;
- $[71] = modelPickerShortcut;
- $[72] = t39;
- } else {
- t39 = $[72];
- }
- let t40;
- if ($[73] !== dimColor || $[74] !== fastModeShortcut) {
- t40 = isFastModeEnabled() && isFastModeAvailable() && {fastModeShortcut} to toggle fast mode;
- $[73] = dimColor;
- $[74] = fastModeShortcut;
- $[75] = t40;
- } else {
- t40 = $[75];
- }
- let t41;
- if ($[76] !== dimColor || $[77] !== stashShortcut) {
- t41 = {stashShortcut} to stash prompt;
- $[76] = dimColor;
- $[77] = stashShortcut;
- $[78] = t41;
- } else {
- t41 = $[78];
- }
- let t42;
- if ($[79] !== dimColor || $[80] !== externalEditorShortcut) {
- t42 = {externalEditorShortcut} to edit in $EDITOR;
- $[79] = dimColor;
- $[80] = externalEditorShortcut;
- $[81] = t42;
- } else {
- t42 = $[81];
- }
- let t43;
- if ($[82] !== dimColor) {
- t43 = isKeybindingCustomizationEnabled() && /keybindings to customize;
- $[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 = {t36}{t37}{t38}{t39}{t40}{t41}{t42}{t43};
- $[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 = {t27}{t35}{t44};
- $[93] = gap;
- $[94] = paddingX;
- $[95] = t27;
- $[96] = t35;
- $[97] = t44;
- $[98] = t45;
- } else {
- t45 = $[98];
- }
- return t45;
+ dimColor?: boolean
+ fixedWidth?: boolean
+ gap?: number
+ paddingX?: number
+}
+
+export function PromptInputHelpMenu(props: Props): React.ReactNode {
+ const { dimColor, fixedWidth, gap, paddingX } = props
+
+ // Get configured shortcuts from keybinding system
+ const transcriptShortcut = formatShortcut(
+ useShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o'),
+ )
+ const todosShortcut = formatShortcut(
+ useShortcutDisplay('app:toggleTodos', 'Global', 'ctrl+t'),
+ )
+ const undoShortcut = formatShortcut(
+ useShortcutDisplay('chat:undo', 'Chat', 'ctrl+_'),
+ )
+ const stashShortcut = formatShortcut(
+ useShortcutDisplay('chat:stash', 'Chat', 'ctrl+s'),
+ )
+ const cycleModeShortcut = formatShortcut(
+ useShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab'),
+ )
+ const modelPickerShortcut = formatShortcut(
+ useShortcutDisplay('chat:modelPicker', 'Chat', 'alt+p'),
+ )
+ const fastModeShortcut = formatShortcut(
+ useShortcutDisplay('chat:fastMode', 'Chat', 'alt+o'),
+ )
+ const externalEditorShortcut = formatShortcut(
+ useShortcutDisplay('chat:externalEditor', 'Chat', 'ctrl+g'),
+ )
+ const terminalShortcut = formatShortcut(
+ useShortcutDisplay('app:toggleTerminal', 'Global', 'meta+j'),
+ )
+ const imagePasteShortcut = formatShortcut(
+ useShortcutDisplay('chat:imagePaste', 'Chat', 'ctrl+v'),
+ )
+
+ // Compute terminal shortcut element outside JSX to satisfy feature() constraint
+ const terminalShortcutElement = feature('TERMINAL_PANEL') ? (
+ getFeatureValue_CACHED_MAY_BE_STALE('tengu_terminal_panel', false) ? (
+
+ {terminalShortcut} for terminal
+
+ ) : null
+ ) : null
+
+ return (
+
+
+
+ ! for bash mode
+
+
+ / for commands
+
+
+ @ for file paths
+
+
+ & for background
+
+
+ /btw for side question
+
+
+
+
+ double tap esc to clear input
+
+
+
+ {cycleModeShortcut}{' '}
+ {process.env.USER_TYPE === 'ant'
+ ? 'to cycle modes'
+ : 'to auto-accept edits'}
+
+
+
+
+ {transcriptShortcut} for verbose output
+
+
+
+ {todosShortcut} to toggle tasks
+
+ {terminalShortcutElement}
+
+ {getNewlineInstructions()}
+
+
+
+
+ {undoShortcut} to undo
+
+ {getPlatform() !== 'windows' && (
+
+ ctrl + z to suspend
+
+ )}
+
+ {imagePasteShortcut} to paste images
+
+
+ {modelPickerShortcut} to switch model
+
+ {isFastModeEnabled() && isFastModeAvailable() && (
+
+
+ {fastModeShortcut} to toggle fast mode
+
+
+ )}
+
+ {stashShortcut} to stash prompt
+
+
+
+ {externalEditorShortcut} to edit in $EDITOR
+
+
+ {isKeybindingCustomizationEnabled() && (
+
+ /keybindings to customize
+
+ )}
+
+
+ )
}
diff --git a/src/components/PromptInput/PromptInputModeIndicator.tsx b/src/components/PromptInput/PromptInputModeIndicator.tsx
index 3ca879ddb..4aa66bf7b 100644
--- a/src/components/PromptInput/PromptInputModeIndicator.tsx
+++ b/src/components/PromptInput/PromptInputModeIndicator.tsx
@@ -1,18 +1,22 @@
-import { c as _c } from "react/compiler-runtime";
-import figures from 'figures';
-import * as React from 'react';
-import { Box, Text } from 'src/ink.js';
-import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from 'src/tools/AgentTool/agentColorManager.js';
-import type { PromptInputMode } from 'src/types/textInputTypes.js';
-import { getTeammateColor } from 'src/utils/teammate.js';
-import type { Theme } from 'src/utils/theme.js';
-import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js';
+import figures from 'figures'
+import * as React from 'react'
+import { Box, Text } from 'src/ink.js'
+import {
+ AGENT_COLOR_TO_THEME_COLOR,
+ AGENT_COLORS,
+ type AgentColorName,
+} from 'src/tools/AgentTool/agentColorManager.js'
+import type { PromptInputMode } from 'src/types/textInputTypes.js'
+import { getTeammateColor } from 'src/utils/teammate.js'
+import type { Theme } from 'src/utils/theme.js'
+import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
+
type Props = {
- mode: PromptInputMode;
- isLoading: boolean;
- viewingAgentName?: string;
- viewingAgentColor?: AgentColorName;
-};
+ mode: PromptInputMode
+ isLoading: boolean
+ viewingAgentName?: string
+ viewingAgentColor?: AgentColorName
+}
/**
* Gets the theme color key for the teammate's assigned color.
@@ -20,73 +24,81 @@ type Props = {
*/
function getTeammateThemeColor(): keyof Theme | undefined {
if (!isAgentSwarmsEnabled()) {
- return undefined;
+ return undefined
}
- const colorName = getTeammateColor();
+ const colorName = getTeammateColor()
if (!colorName) {
- return undefined;
+ return undefined
}
if (AGENT_COLORS.includes(colorName as AgentColorName)) {
- return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName];
+ return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]
}
- return undefined;
+ return undefined
}
+
type PromptCharProps = {
- isLoading: boolean;
+ isLoading: boolean
// Dead code elimination: parameter named themeColor to avoid "teammate" string in external builds
- themeColor?: keyof Theme;
-};
+ themeColor?: keyof Theme
+}
/**
* Renders the prompt character (❯).
* Teammate color overrides the default color when set.
*/
-function PromptChar(t0) {
- const $ = _c(3);
- const {
- isLoading,
- themeColor
- } = t0;
- const teammateColor = themeColor;
- const color = teammateColor ?? (false ? "subtle" : undefined);
- let t1;
- if ($[0] !== color || $[1] !== isLoading) {
- t1 = {figures.pointer} ;
- $[0] = color;
- $[1] = isLoading;
- $[2] = t1;
- } else {
- t1 = $[2];
- }
- return t1;
+function PromptChar({
+ isLoading,
+ themeColor,
+}: PromptCharProps): React.ReactNode {
+ // Assign to original name for clarity within the function
+ const teammateColor = themeColor
+ const isAnt = process.env.USER_TYPE === 'ant'
+ const color = teammateColor ?? (isAnt ? 'subtle' : undefined)
+
+ return (
+
+ {figures.pointer}
+
+ )
}
-export function PromptInputModeIndicator(t0) {
- const $ = _c(6);
- const {
- mode,
- isLoading,
- viewingAgentName,
- viewingAgentColor
- } = t0;
- let t1;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = getTeammateThemeColor();
- $[0] = t1;
- } else {
- t1 = $[0];
- }
- const teammateColor = t1;
- const viewedTeammateThemeColor = viewingAgentColor ? AGENT_COLOR_TO_THEME_COLOR[viewingAgentColor] : undefined;
- let t2;
- if ($[1] !== isLoading || $[2] !== mode || $[3] !== viewedTeammateThemeColor || $[4] !== viewingAgentName) {
- t2 = {viewingAgentName ? : mode === "bash" ? ! : };
- $[1] = isLoading;
- $[2] = mode;
- $[3] = viewedTeammateThemeColor;
- $[4] = viewingAgentName;
- $[5] = t2;
- } else {
- t2 = $[5];
- }
- return t2;
+
+export function PromptInputModeIndicator({
+ mode,
+ isLoading,
+ viewingAgentName,
+ viewingAgentColor,
+}: Props): React.ReactNode {
+ const teammateColor = getTeammateThemeColor()
+
+ // Convert viewed teammate's color to theme color
+ // Falls back to PromptChar's default (subtle for ants, undefined for external)
+ const viewedTeammateThemeColor = viewingAgentColor
+ ? AGENT_COLOR_TO_THEME_COLOR[viewingAgentColor]
+ : undefined
+
+ return (
+
+ {viewingAgentName ? (
+ // Use teammate's color on the standard prompt character, matching established style
+
+ ) : mode === 'bash' ? (
+
+ !
+
+ ) : (
+
+ )}
+
+ )
}
diff --git a/src/components/PromptInput/PromptInputQueuedCommands.tsx b/src/components/PromptInput/PromptInputQueuedCommands.tsx
index 9fe1ad571..c4637b803 100644
--- a/src/components/PromptInput/PromptInputQueuedCommands.tsx
+++ b/src/components/PromptInput/PromptInputQueuedCommands.tsx
@@ -1,17 +1,26 @@
-import { feature } from 'bun:bundle';
-import * as React from 'react';
-import { useMemo } from 'react';
-import { Box } from 'src/ink.js';
-import { useAppState } from 'src/state/AppState.js';
-import { STATUS_TAG, SUMMARY_TAG, TASK_NOTIFICATION_TAG } from '../../constants/xml.js';
-import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js';
-import { useCommandQueue } from '../../hooks/useCommandQueue.js';
-import type { QueuedCommand } from '../../types/textInputTypes.js';
-import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js';
-import { createUserMessage, EMPTY_LOOKUPS, normalizeMessages } from '../../utils/messages.js';
-import { jsonParse } from '../../utils/slowOperations.js';
-import { Message } from '../Message.js';
-const EMPTY_SET = new Set();
+import { feature } from 'bun:bundle'
+import * as React from 'react'
+import { useMemo } from 'react'
+import { Box } from 'src/ink.js'
+import { useAppState } from 'src/state/AppState.js'
+import {
+ STATUS_TAG,
+ SUMMARY_TAG,
+ TASK_NOTIFICATION_TAG,
+} from '../../constants/xml.js'
+import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js'
+import { useCommandQueue } from '../../hooks/useCommandQueue.js'
+import type { QueuedCommand } from '../../types/textInputTypes.js'
+import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js'
+import {
+ createUserMessage,
+ EMPTY_LOOKUPS,
+ normalizeMessages,
+} from '../../utils/messages.js'
+import { jsonParse } from '../../utils/slowOperations.js'
+import { Message } from '../Message.js'
+
+const EMPTY_SET = new Set()
/**
* Check if a command value is an idle notification that should be hidden.
@@ -19,15 +28,15 @@ const EMPTY_SET = new Set();
*/
function isIdleNotification(value: string): boolean {
try {
- const parsed = jsonParse(value);
- return parsed?.type === 'idle_notification';
+ const parsed = jsonParse(value)
+ return parsed?.type === 'idle_notification'
} catch {
- return false;
+ return false
}
}
// Maximum number of task notification lines to show
-const MAX_VISIBLE_NOTIFICATIONS = 3;
+const MAX_VISIBLE_NOTIFICATIONS = 3
/**
* Create a synthetic overflow notification message for capped task notifications.
@@ -36,7 +45,7 @@ function createOverflowNotificationMessage(count: number): string {
return `<${TASK_NOTIFICATION_TAG}>
<${SUMMARY_TAG}>+${count} more tasks completed${SUMMARY_TAG}>
<${STATUS_TAG}>completed${STATUS_TAG}>
-${TASK_NOTIFICATION_TAG}>`;
+${TASK_NOTIFICATION_TAG}>`
}
/**
@@ -44,73 +53,114 @@ function createOverflowNotificationMessage(count: number): string {
* Other command types are always shown in full.
* Idle notifications are filtered out entirely.
*/
-function processQueuedCommands(queuedCommands: QueuedCommand[]): QueuedCommand[] {
+function processQueuedCommands(
+ queuedCommands: QueuedCommand[],
+): QueuedCommand[] {
// Filter out idle notifications - they are processed silently
- const filteredCommands = queuedCommands.filter(cmd => typeof cmd.value !== 'string' || !isIdleNotification(cmd.value));
+ const filteredCommands = queuedCommands.filter(
+ cmd => typeof cmd.value !== 'string' || !isIdleNotification(cmd.value),
+ )
// Separate task notifications from other commands
- const taskNotifications = filteredCommands.filter(cmd => cmd.mode === 'task-notification');
- const otherCommands = filteredCommands.filter(cmd => cmd.mode !== 'task-notification');
+ const taskNotifications = filteredCommands.filter(
+ cmd => cmd.mode === 'task-notification',
+ )
+ const otherCommands = filteredCommands.filter(
+ cmd => cmd.mode !== 'task-notification',
+ )
// If notifications fit within limit, return all commands as-is
if (taskNotifications.length <= MAX_VISIBLE_NOTIFICATIONS) {
- return [...otherCommands, ...taskNotifications];
+ return [...otherCommands, ...taskNotifications]
}
// Show first (MAX_VISIBLE_NOTIFICATIONS - 1) notifications, then a summary
- const visibleNotifications = taskNotifications.slice(0, MAX_VISIBLE_NOTIFICATIONS - 1);
- const overflowCount = taskNotifications.length - (MAX_VISIBLE_NOTIFICATIONS - 1);
+ const visibleNotifications = taskNotifications.slice(
+ 0,
+ MAX_VISIBLE_NOTIFICATIONS - 1,
+ )
+ const overflowCount =
+ taskNotifications.length - (MAX_VISIBLE_NOTIFICATIONS - 1)
// Create synthetic overflow message
const overflowCommand: QueuedCommand = {
value: createOverflowNotificationMessage(overflowCount),
- mode: 'task-notification'
- };
- return [...otherCommands, ...visibleNotifications, overflowCommand];
+ mode: 'task-notification',
+ }
+
+ return [...otherCommands, ...visibleNotifications, overflowCommand]
}
+
function PromptInputQueuedCommandsImpl(): React.ReactNode {
- const queuedCommands = useCommandQueue();
- const viewingAgent = useAppState(s => !!s.viewingAgentTaskId);
+ const queuedCommands = useCommandQueue()
+ const viewingAgent = useAppState(s => !!s.viewingAgentTaskId)
// Brief layout: dim queue items + skip the paddingX (brief messages
// already indent themselves). Gate mirrors the brief-spinner/message
// check elsewhere — no teammate-view override needed since this
// component early-returns when viewing a teammate.
- const useBriefLayout = feature('KAIROS') || feature('KAIROS_BRIEF') ?
- // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
- useAppState(s_0 => s_0.isBriefOnly) : false;
+ const useBriefLayout =
+ feature('KAIROS') || feature('KAIROS_BRIEF')
+ ? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
+ useAppState(s => s.isBriefOnly)
+ : false
// createUserMessage mints a fresh UUID per call; without memoization, streaming
// re-renders defeat Message's areMessagePropsEqual (compares uuid) → flicker.
const messages = useMemo(() => {
- if (queuedCommands.length === 0) return null;
+ if (queuedCommands.length === 0) return null
// task-notification is shown via useInboxNotification; most isMeta commands
// (scheduled tasks, proactive ticks) are system-generated and hidden.
// Channel messages are the exception — isMeta but shown so the keyboard
// user sees what arrived.
- const visibleCommands = queuedCommands.filter(isQueuedCommandVisible);
- if (visibleCommands.length === 0) return null;
- const processedCommands = processQueuedCommands(visibleCommands);
- return normalizeMessages(processedCommands.map(cmd => {
- let content = cmd.value;
- if (cmd.mode === 'bash' && typeof content === 'string') {
- content = `${content}`;
- }
- // [Image #N] placeholders are inline in the text value (inserted at
- // paste time), so the queue preview shows them without stub blocks.
- return createUserMessage({
- content
- });
- }));
- }, [queuedCommands]);
+ const visibleCommands = queuedCommands.filter(isQueuedCommandVisible)
+ if (visibleCommands.length === 0) return null
+ const processedCommands = processQueuedCommands(visibleCommands)
+ return normalizeMessages(
+ processedCommands.map(cmd => {
+ let content = cmd.value
+ if (cmd.mode === 'bash' && typeof content === 'string') {
+ content = `${content}`
+ }
+ // [Image #N] placeholders are inline in the text value (inserted at
+ // paste time), so the queue preview shows them without stub blocks.
+ return createUserMessage({ content })
+ }),
+ )
+ }, [queuedCommands])
// Don't show leader's queued commands when viewing any agent's transcript
if (viewingAgent || messages === null) {
- return null;
+ return null
}
- return
- {messages.map((message, i) =>
-
- )}
- ;
+
+ return (
+
+ {messages.map((message, i) => (
+
+
+
+ ))}
+
+ )
}
-export const PromptInputQueuedCommands = React.memo(PromptInputQueuedCommandsImpl);
+
+export const PromptInputQueuedCommands = React.memo(
+ PromptInputQueuedCommandsImpl,
+)
diff --git a/src/components/PromptInput/PromptInputStashNotice.tsx b/src/components/PromptInput/PromptInputStashNotice.tsx
index cf01db045..8a44e8607 100644
--- a/src/components/PromptInput/PromptInputStashNotice.tsx
+++ b/src/components/PromptInput/PromptInputStashNotice.tsx
@@ -1,24 +1,21 @@
-import { c as _c } from "react/compiler-runtime";
-import figures from 'figures';
-import * as React from 'react';
-import { Box, Text } from 'src/ink.js';
+import figures from 'figures'
+import * as React from 'react'
+import { Box, Text } from 'src/ink.js'
+
type Props = {
- hasStash: boolean;
-};
-export function PromptInputStashNotice(t0) {
- const $ = _c(1);
- const {
- hasStash
- } = t0;
- if (!hasStash) {
- return null;
- }
- let t1;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = {figures.pointerSmall} Stashed (auto-restores after submit);
- $[0] = t1;
- } else {
- t1 = $[0];
- }
- return t1;
+ hasStash: boolean
+}
+
+export function PromptInputStashNotice({ hasStash }: Props): React.ReactNode {
+ if (!hasStash) {
+ return null
+ }
+
+ return (
+
+
+ {figures.pointerSmall} Stashed (auto-restores after submit)
+
+
+ )
}
diff --git a/src/components/PromptInput/SandboxPromptFooterHint.tsx b/src/components/PromptInput/SandboxPromptFooterHint.tsx
index 43b81fcfa..1324a9832 100644
--- a/src/components/PromptInput/SandboxPromptFooterHint.tsx
+++ b/src/components/PromptInput/SandboxPromptFooterHint.tsx
@@ -1,63 +1,61 @@
-import { c as _c } from "react/compiler-runtime";
-import * as React from 'react';
-import { type ReactNode, useEffect, useRef, useState } from 'react';
-import { Box, Text } from '../../ink.js';
-import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
-import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
-export function SandboxPromptFooterHint() {
- const $ = _c(6);
- const [recentViolationCount, setRecentViolationCount] = useState(0);
- const timerRef = useRef(null);
- const detailsShortcut = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
- let t0;
- let t1;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t0 = () => {
- if (!SandboxManager.isSandboxingEnabled()) {
- return;
- }
- const store = SandboxManager.getSandboxViolationStore();
- let lastCount = store.getTotalCount();
- const unsubscribe = store.subscribe(() => {
- const currentCount = store.getTotalCount();
- const newViolations = currentCount - lastCount;
- if (newViolations > 0) {
- setRecentViolationCount(newViolations);
- lastCount = currentCount;
- if (timerRef.current) {
- clearTimeout(timerRef.current);
- }
- timerRef.current = setTimeout(setRecentViolationCount, 5000, 0);
- }
- });
- return () => {
- unsubscribe();
+import * as React from 'react'
+import { type ReactNode, useEffect, useRef, useState } from 'react'
+import { Box, Text } from '../../ink.js'
+import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
+import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
+
+export function SandboxPromptFooterHint(): ReactNode {
+ const [recentViolationCount, setRecentViolationCount] = useState(0)
+ const timerRef = useRef(null)
+ const detailsShortcut = useShortcutDisplay(
+ 'app:toggleTranscript',
+ 'Global',
+ 'ctrl+o',
+ )
+
+ useEffect(() => {
+ if (!SandboxManager.isSandboxingEnabled()) {
+ return
+ }
+
+ const store = SandboxManager.getSandboxViolationStore()
+ let lastCount = store.getTotalCount()
+
+ const unsubscribe = store.subscribe(() => {
+ const currentCount = store.getTotalCount()
+ const newViolations = currentCount - lastCount
+
+ if (newViolations > 0) {
+ setRecentViolationCount(newViolations)
+ lastCount = currentCount
+
if (timerRef.current) {
- clearTimeout(timerRef.current);
+ clearTimeout(timerRef.current)
}
- };
- };
- t1 = [];
- $[0] = t0;
- $[1] = t1;
- } else {
- t0 = $[0];
- t1 = $[1];
- }
- useEffect(t0, t1);
+
+ timerRef.current = setTimeout(setRecentViolationCount, 5000, 0)
+ }
+ })
+
+ return () => {
+ unsubscribe()
+ if (timerRef.current) {
+ clearTimeout(timerRef.current)
+ }
+ }
+ }, [])
+
if (!SandboxManager.isSandboxingEnabled() || recentViolationCount === 0) {
- return null;
+ return null
}
- const t2 = recentViolationCount === 1 ? "operation" : "operations";
- let t3;
- if ($[2] !== detailsShortcut || $[3] !== recentViolationCount || $[4] !== t2) {
- t3 = ⧈ Sandbox blocked {recentViolationCount}{" "}{t2} ·{" "}{detailsShortcut} for details · /sandbox to disable;
- $[2] = detailsShortcut;
- $[3] = recentViolationCount;
- $[4] = t2;
- $[5] = t3;
- } else {
- t3 = $[5];
- }
- return t3;
+
+ return (
+
+
+ ⧈ Sandbox blocked {recentViolationCount}{' '}
+ {recentViolationCount === 1 ? 'operation' : 'operations'} ·{' '}
+ {detailsShortcut} for details · /sandbox to disable
+
+
+ )
}
diff --git a/src/components/PromptInput/ShimmeredInput.tsx b/src/components/PromptInput/ShimmeredInput.tsx
index 5fd163e04..11da7ad76 100644
--- a/src/components/PromptInput/ShimmeredInput.tsx
+++ b/src/components/PromptInput/ShimmeredInput.tsx
@@ -1,142 +1,121 @@
-import { c as _c } from "react/compiler-runtime";
-import * as React from 'react';
-import { Ansi, Box, Text, useAnimationFrame } from '../../ink.js';
-import { segmentTextByHighlights, type TextHighlight } from '../../utils/textHighlighting.js';
-import { ShimmerChar } from '../Spinner/ShimmerChar.js';
+import * as React from 'react'
+import { Ansi, Box, Text, useAnimationFrame } from '../../ink.js'
+import {
+ segmentTextByHighlights,
+ type TextHighlight,
+} from '../../utils/textHighlighting.js'
+import { ShimmerChar } from '../Spinner/ShimmerChar.js'
+
type Props = {
- text: string;
- highlights: TextHighlight[];
-};
+ text: string
+ highlights: TextHighlight[]
+}
+
type LinePart = {
- text: string;
- highlight: TextHighlight | undefined;
- start: number;
-};
-export function HighlightedInput(t0) {
- const $ = _c(23);
- const {
- text,
- highlights
- } = t0;
- let lines;
- if ($[0] !== highlights || $[1] !== text) {
- const segments = segmentTextByHighlights(text, highlights);
- lines = [[]];
- let pos = 0;
+ text: string
+ highlight: TextHighlight | undefined
+ start: number
+}
+
+export function HighlightedInput({ text, highlights }: Props): React.ReactNode {
+ // The shimmer animation (below) re-renders this component at 20fps while the
+ // ultrathink keyword is present. text/highlights are referentially stable
+ // across animation ticks (parent doesn't re-render), so memoize everything
+ // that derives from them: segmentTextByHighlights alone is ~85µs/call
+ // (tokenize + sort + O(n²) overlap), which adds up fast at 20fps.
+ const { lines, hasShimmer, sweepStart, cycleLength } = React.useMemo(() => {
+ const segments = segmentTextByHighlights(text, highlights)
+
+ // Split segments by newlines into per-line groups. Ink's row-direction Box
+ // indents continuation lines of a multi-line child to that child's X offset.
+ // By splitting at newlines, each line renders as its own row, avoiding the
+ // incorrect indentation when highlighted text is followed by wrapped content.
+ const lines: LinePart[][] = [[]]
+ let pos = 0
for (const segment of segments) {
- const parts = segment.text.split("\n");
+ const parts = segment.text.split('\n')
for (let i = 0; i < parts.length; i++) {
if (i > 0) {
- lines.push([]);
- pos = pos + 1;
+ lines.push([])
+ pos += 1
}
- const part = parts[i];
+ const part = parts[i]!
if (part.length > 0) {
- lines[lines.length - 1].push({
+ lines[lines.length - 1]!.push({
text: part,
highlight: segment.highlight,
- start: pos
- });
+ start: pos,
+ })
}
- pos = pos + part.length;
+ pos += part.length
}
}
- $[0] = highlights;
- $[1] = text;
- $[2] = lines;
- } else {
- lines = $[2];
- }
- let t1;
- if ($[3] !== highlights) {
- t1 = highlights.some(_temp);
- $[3] = highlights;
- $[4] = t1;
- } else {
- t1 = $[4];
- }
- const hasShimmer = t1;
- let sweepStart = 0;
- let cycleLength = 1;
- if (hasShimmer) {
- let lo = Infinity;
- let hi = -Infinity;
- if ($[5] !== hi || $[6] !== highlights || $[7] !== lo) {
- for (const h_0 of highlights) {
- if (h_0.shimmerColor) {
- lo = Math.min(lo, h_0.start);
- hi = Math.max(hi, h_0.end);
+
+ // Scope the sweep to shimmer-highlighted ranges so cycle time doesn't grow
+ // with input length. Padding creates an offscreen pause between sweeps.
+ const hasShimmer = highlights.some(h => h.shimmerColor)
+ let sweepStart = 0
+ let cycleLength = 1
+ if (hasShimmer) {
+ const padding = 10
+ let lo = Infinity
+ let hi = -Infinity
+ for (const h of highlights) {
+ if (h.shimmerColor) {
+ lo = Math.min(lo, h.start)
+ hi = Math.max(hi, h.end)
}
}
- $[5] = hi;
- $[6] = highlights;
- $[7] = lo;
- $[8] = lo;
- $[9] = hi;
- } else {
- lo = $[8] as number;
- hi = $[9] as number;
+ sweepStart = lo - padding
+ cycleLength = hi - lo + padding * 2
}
- sweepStart = lo - 10;
- cycleLength = hi - lo + 20;
- }
- let t2;
- if ($[10] !== cycleLength || $[11] !== hasShimmer || $[12] !== lines || $[13] !== sweepStart) {
- t2 = {
- lines,
- hasShimmer,
- sweepStart,
- cycleLength
- };
- $[10] = cycleLength;
- $[11] = hasShimmer;
- $[12] = lines;
- $[13] = sweepStart;
- $[14] = t2;
- } else {
- t2 = $[14];
- }
- const {
- lines: lines_0,
- hasShimmer: hasShimmer_0,
- sweepStart: sweepStart_0,
- cycleLength: cycleLength_0
- } = t2;
- const [ref, time] = useAnimationFrame(hasShimmer_0 ? 50 : null);
- const glimmerIndex = hasShimmer_0 ? sweepStart_0 + Math.floor(time / 50) % cycleLength_0 : -100;
- let t3;
- if ($[15] !== glimmerIndex || $[16] !== lines_0) {
- let t4;
- if ($[18] !== glimmerIndex) {
- t4 = (lineParts, lineIndex) => {lineParts.length === 0 ? : lineParts.map((part_0, partIndex) => {
- if (part_0.highlight?.shimmerColor && part_0.highlight.color) {
- return {part_0.text.split("").map((char, charIndex) => )};
- }
- return {part_0.text};
- })};
- $[18] = glimmerIndex;
- $[19] = t4;
- } else {
- t4 = $[19];
- }
- t3 = lines_0.map(t4);
- $[15] = glimmerIndex;
- $[16] = lines_0;
- $[17] = t3;
- } else {
- t3 = $[17];
- }
- let t4;
- if ($[20] !== ref || $[21] !== t3) {
- t4 = {t3};
- $[20] = ref;
- $[21] = t3;
- $[22] = t4;
- } else {
- t4 = $[22];
- }
- return t4;
-}
-function _temp(h) {
- return h.shimmerColor;
+
+ return { lines, hasShimmer, sweepStart, cycleLength }
+ }, [text, highlights])
+
+ const [ref, time] = useAnimationFrame(hasShimmer ? 50 : null)
+ const glimmerIndex = hasShimmer
+ ? sweepStart + (Math.floor(time / 50) % cycleLength)
+ : -100
+
+ return (
+
+ {lines.map((lineParts, lineIndex) => (
+
+ {lineParts.length === 0 ? (
+
+ ) : (
+ lineParts.map((part, partIndex) => {
+ if (part.highlight?.shimmerColor && part.highlight.color) {
+ return (
+
+ {part.text.split('').map((char, charIndex) => (
+
+ ))}
+
+ )
+ }
+ return (
+
+ {part.text}
+
+ )
+ })
+ )}
+
+ ))}
+
+ )
}
diff --git a/src/components/PromptInput/VoiceIndicator.tsx b/src/components/PromptInput/VoiceIndicator.tsx
index 9bf0a6d8b..6dc73baf2 100644
--- a/src/components/PromptInput/VoiceIndicator.tsx
+++ b/src/components/PromptInput/VoiceIndicator.tsx
@@ -1,73 +1,32 @@
-import { c as _c } from "react/compiler-runtime";
-import { feature } from 'bun:bundle';
-import * as React from 'react';
-import { useSettings } from '../../hooks/useSettings.js';
-import { Box, Text, useAnimationFrame } from '../../ink.js';
-import { interpolateColor, toRGBColor } from '../Spinner/utils.js';
+import { feature } from 'bun:bundle'
+import * as React from 'react'
+import { useSettings } from '../../hooks/useSettings.js'
+import { Box, Text, useAnimationFrame } from '../../ink.js'
+import { interpolateColor, toRGBColor } from '../Spinner/utils.js'
+
type Props = {
- voiceState: 'idle' | 'recording' | 'processing';
-};
+ voiceState: 'idle' | 'recording' | 'processing'
+}
// Processing shimmer colors: dim gray to lighter gray (matches ThinkingShimmerText)
-const PROCESSING_DIM = {
- r: 153,
- g: 153,
- b: 153
-};
-const PROCESSING_BRIGHT = {
- r: 185,
- g: 185,
- b: 185
-};
-const PULSE_PERIOD_S = 2; // 2 second period for all pulsing animations
+const PROCESSING_DIM = { r: 153, g: 153, b: 153 }
+const PROCESSING_BRIGHT = { r: 185, g: 185, b: 185 }
-export function VoiceIndicator(props) {
- const $ = _c(2);
- if (!feature("VOICE_MODE")) {
- return null;
- }
- let t0;
- if ($[0] !== props) {
- t0 = ;
- $[0] = props;
- $[1] = t0;
- } else {
- t0 = $[1];
- }
- return t0;
+const PULSE_PERIOD_S = 2 // 2 second period for all pulsing animations
+
+export function VoiceIndicator(props: Props): React.ReactNode {
+ if (!feature('VOICE_MODE')) return null
+ return
}
-function VoiceIndicatorImpl(t0) {
- const $ = _c(2);
- const {
- voiceState
- } = t0;
+
+function VoiceIndicatorImpl({ voiceState }: Props): React.ReactNode {
switch (voiceState) {
- case "recording":
- {
- let t1;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = listening…;
- $[0] = t1;
- } else {
- t1 = $[0];
- }
- return t1;
- }
- case "processing":
- {
- let t1;
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = ;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- return t1;
- }
- case "idle":
- {
- return null;
- }
+ case 'recording':
+ return listening…
+ case 'processing':
+ return
+ case 'idle':
+ return null
}
}
@@ -75,62 +34,30 @@ function VoiceIndicatorImpl(t0) {
// is too brief for a 1s-period shimmer to register, and a 50ms animation
// timer here runs concurrently with auto-repeat spaces arriving every
// 30-80ms, compounding re-renders during an already-busy window.
-export function VoiceWarmupHint() {
- const $ = _c(1);
- if (!feature("VOICE_MODE")) {
- return null;
- }
- let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t0 = keep holding…;
- $[0] = t0;
- } else {
- t0 = $[0];
- }
- return t0;
+export function VoiceWarmupHint(): React.ReactNode {
+ if (!feature('VOICE_MODE')) return null
+ return keep holding…
}
-function ProcessingShimmer() {
- const $ = _c(8);
- const settings = useSettings();
- const reducedMotion = settings.prefersReducedMotion ?? false;
- const [ref, time] = useAnimationFrame(reducedMotion ? null : 50);
+
+function ProcessingShimmer(): React.ReactNode {
+ const settings = useSettings()
+ const reducedMotion = settings.prefersReducedMotion ?? false
+ const [ref, time] = useAnimationFrame(reducedMotion ? null : 50)
+
if (reducedMotion) {
- let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t0 = Voice: processing…;
- $[0] = t0;
- } else {
- t0 = $[0];
- }
- return t0;
+ return Voice: processing…
}
- const elapsedSec = time / 1000;
- const opacity = (Math.sin(elapsedSec * Math.PI * 2 / PULSE_PERIOD_S) + 1) / 2;
- let t0;
- if ($[1] !== opacity) {
- t0 = toRGBColor(interpolateColor(PROCESSING_DIM, PROCESSING_BRIGHT, opacity));
- $[1] = opacity;
- $[2] = t0;
- } else {
- t0 = $[2];
- }
- const color = t0;
- let t1;
- if ($[3] !== color) {
- t1 = Voice: processing…;
- $[3] = color;
- $[4] = t1;
- } else {
- t1 = $[4];
- }
- let t2;
- if ($[5] !== ref || $[6] !== t1) {
- t2 = {t1};
- $[5] = ref;
- $[6] = t1;
- $[7] = t2;
- } else {
- t2 = $[7];
- }
- return t2;
+
+ const elapsedSec = time / 1000
+ const opacity =
+ (Math.sin((elapsedSec * Math.PI * 2) / PULSE_PERIOD_S) + 1) / 2
+ const color = toRGBColor(
+ interpolateColor(PROCESSING_DIM, PROCESSING_BRIGHT, opacity),
+ )
+
+ return (
+
+ Voice: processing…
+
+ )
}
diff --git a/src/components/agents/AgentDetail.tsx b/src/components/agents/AgentDetail.tsx
index 4c0f50e56..4c817b134 100644
--- a/src/components/agents/AgentDetail.tsx
+++ b/src/components/agents/AgentDetail.tsx
@@ -1,219 +1,148 @@
-import { c as _c } from "react/compiler-runtime";
-import figures from 'figures';
-import * as React from 'react';
-import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
-import { Box, Text } from '../../ink.js';
-import { useKeybinding } from '../../keybindings/useKeybinding.js';
-import type { Tools } from '../../Tool.js';
-import { getAgentColor } from '../../tools/AgentTool/agentColorManager.js';
-import { getMemoryScopeDisplay } from '../../tools/AgentTool/agentMemory.js';
-import { resolveAgentTools } from '../../tools/AgentTool/agentToolUtils.js';
-import { type AgentDefinition, isBuiltInAgent } from '../../tools/AgentTool/loadAgentsDir.js';
-import { getAgentModelDisplay } from '../../utils/model/agent.js';
-import { Markdown } from '../Markdown.js';
-import { getActualRelativeAgentFilePath } from './agentFileUtils.js';
+import figures from 'figures'
+import * as React from 'react'
+import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
+import { Box, Text } from '../../ink.js'
+import { useKeybinding } from '../../keybindings/useKeybinding.js'
+import type { Tools } from '../../Tool.js'
+import { getAgentColor } from '../../tools/AgentTool/agentColorManager.js'
+import { getMemoryScopeDisplay } from '../../tools/AgentTool/agentMemory.js'
+import { resolveAgentTools } from '../../tools/AgentTool/agentToolUtils.js'
+import {
+ type AgentDefinition,
+ isBuiltInAgent,
+} from '../../tools/AgentTool/loadAgentsDir.js'
+import { getAgentModelDisplay } from '../../utils/model/agent.js'
+import { Markdown } from '../Markdown.js'
+import { getActualRelativeAgentFilePath } from './agentFileUtils.js'
+
type Props = {
- agent: AgentDefinition;
- tools: Tools;
- allAgents?: AgentDefinition[];
- onBack: () => void;
-};
-export function AgentDetail(t0) {
- const $ = _c(48);
- const {
- agent,
- tools,
- onBack
- } = t0;
- const resolvedTools = resolveAgentTools(agent, tools, false);
- let t1;
- if ($[0] !== agent) {
- t1 = getActualRelativeAgentFilePath(agent);
- $[0] = agent;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- const filePath = t1;
- let t2;
- if ($[2] !== agent.agentType) {
- t2 = getAgentColor(agent.agentType);
- $[2] = agent.agentType;
- $[3] = t2;
- } else {
- t2 = $[3];
- }
- const backgroundColor = t2;
- let t3;
- if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
- t3 = {
- context: "Confirmation"
- };
- $[4] = t3;
- } else {
- t3 = $[4];
- }
- useKeybinding("confirm:no", onBack, t3);
- let t4;
- if ($[5] !== onBack) {
- t4 = e => {
- if (e.key === "return") {
- e.preventDefault();
- onBack();
- }
- };
- $[5] = onBack;
- $[6] = t4;
- } else {
- t4 = $[6];
- }
- const handleKeyDown = t4;
- const renderToolsList = function renderToolsList() {
- if (resolvedTools.hasWildcard) {
- return All tools;
- }
- if (!agent.tools || agent.tools.length === 0) {
- return None;
- }
- return <>{resolvedTools.validTools.length > 0 && {resolvedTools.validTools.join(", ")}}{resolvedTools.invalidTools.length > 0 && {figures.warning} Unrecognized:{" "}{resolvedTools.invalidTools.join(", ")}}>;
- };
- const T0 = Box;
- const t5 = "column";
- const t6 = 1;
- const t7 = 0;
- const t8 = true;
- let t9;
- if ($[7] !== filePath) {
- t9 = {filePath};
- $[7] = filePath;
- $[8] = t9;
- } else {
- t9 = $[8];
- }
- let t10;
- if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
- t10 = Description (tells Claude when to use this agent):;
- $[9] = t10;
- } else {
- t10 = $[9];
- }
- let t11;
- if ($[10] !== agent.whenToUse) {
- t11 = {t10}{agent.whenToUse};
- $[10] = agent.whenToUse;
- $[11] = t11;
- } else {
- t11 = $[11];
- }
- const T1 = Box;
- let t12;
- if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
- t12 = Tools:{" "};
- $[12] = t12;
- } else {
- t12 = $[12];
- }
- const t13 = renderToolsList();
- let t14;
- if ($[13] !== T1 || $[14] !== t12 || $[15] !== t13) {
- t14 = {t12}{t13};
- $[13] = T1;
- $[14] = t12;
- $[15] = t13;
- $[16] = t14;
- } else {
- t14 = $[16];
- }
- let t15;
- if ($[17] === Symbol.for("react.memo_cache_sentinel")) {
- t15 = Model;
- $[17] = t15;
- } else {
- t15 = $[17];
- }
- let t16;
- if ($[18] !== agent.model) {
- t16 = getAgentModelDisplay(agent.model);
- $[18] = agent.model;
- $[19] = t16;
- } else {
- t16 = $[19];
- }
- let t17;
- if ($[20] !== t16) {
- t17 = {t15}: {t16};
- $[20] = t16;
- $[21] = t17;
- } else {
- t17 = $[21];
- }
- let t18;
- if ($[22] !== agent.permissionMode) {
- t18 = agent.permissionMode && Permission mode: {agent.permissionMode};
- $[22] = agent.permissionMode;
- $[23] = t18;
- } else {
- t18 = $[23];
- }
- let t19;
- if ($[24] !== agent.memory) {
- t19 = agent.memory && Memory: {getMemoryScopeDisplay(agent.memory)};
- $[24] = agent.memory;
- $[25] = t19;
- } else {
- t19 = $[25];
- }
- let t20;
- if ($[26] !== agent.hooks) {
- t20 = agent.hooks && Object.keys(agent.hooks).length > 0 && Hooks: {Object.keys(agent.hooks).join(", ")};
- $[26] = agent.hooks;
- $[27] = t20;
- } else {
- t20 = $[27];
- }
- let t21;
- if ($[28] !== agent.skills) {
- t21 = agent.skills && agent.skills.length > 0 && Skills:{" "}{agent.skills.length > 10 ? `${agent.skills.length} skills` : agent.skills.join(", ")};
- $[28] = agent.skills;
- $[29] = t21;
- } else {
- t21 = $[29];
- }
- let t22;
- if ($[30] !== agent.agentType || $[31] !== backgroundColor) {
- t22 = backgroundColor && Color:{" "}{" "}{agent.agentType}{" "};
- $[30] = agent.agentType;
- $[31] = backgroundColor;
- $[32] = t22;
- } else {
- t22 = $[32];
- }
- let t23;
- if ($[33] !== agent) {
- t23 = !isBuiltInAgent(agent) && <>System prompt:{agent.getSystemPrompt()}>;
- $[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 = {t9}{t11}{t14}{t17}{t18}{t19}{t20}{t21}{t22}{t23};
- $[35] = T0;
- $[36] = handleKeyDown;
- $[37] = t11;
- $[38] = t14;
- $[39] = t17;
- $[40] = t18;
- $[41] = t19;
- $[42] = t20;
- $[43] = t21;
- $[44] = t22;
- $[45] = t23;
- $[46] = t9;
- $[47] = t24;
- } else {
- t24 = $[47];
- }
- return t24;
+ agent: AgentDefinition
+ tools: Tools
+ allAgents?: AgentDefinition[]
+ onBack: () => void
+}
+
+export function AgentDetail({ agent, tools, onBack }: Props): React.ReactNode {
+ const resolvedTools = resolveAgentTools(agent, tools, false)
+ const filePath = getActualRelativeAgentFilePath(agent)
+ const backgroundColor = getAgentColor(agent.agentType)
+
+ // Handle Esc to go back
+ useKeybinding('confirm:no', onBack, { context: 'Confirmation' })
+
+ // Handle Enter to go back
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'return') {
+ e.preventDefault()
+ onBack()
+ }
+ }
+
+ function renderToolsList(): React.ReactNode {
+ if (resolvedTools.hasWildcard) {
+ return All tools
+ }
+
+ if (!agent.tools || agent.tools.length === 0) {
+ return None
+ }
+
+ return (
+ <>
+ {resolvedTools.validTools.length > 0 && (
+ {resolvedTools.validTools.join(', ')}
+ )}
+ {resolvedTools.invalidTools.length > 0 && (
+
+ {figures.warning} Unrecognized:{' '}
+ {resolvedTools.invalidTools.join(', ')}
+
+ )}
+ >
+ )
+ }
+
+ return (
+
+ {filePath}
+
+
+
+ Description (tells Claude when to use this agent):
+
+
+ {agent.whenToUse}
+
+
+
+
+
+ Tools:{' '}
+
+ {renderToolsList()}
+
+
+
+ Model: {getAgentModelDisplay(agent.model)}
+
+
+ {agent.permissionMode && (
+
+ Permission mode: {agent.permissionMode}
+
+ )}
+
+ {agent.memory && (
+
+ Memory: {getMemoryScopeDisplay(agent.memory)}
+
+ )}
+
+ {agent.hooks && Object.keys(agent.hooks).length > 0 && (
+
+ Hooks: {Object.keys(agent.hooks).join(', ')}
+
+ )}
+
+ {agent.skills && agent.skills.length > 0 && (
+
+ Skills:{' '}
+ {agent.skills.length > 10
+ ? `${agent.skills.length} skills`
+ : agent.skills.join(', ')}
+
+ )}
+
+ {backgroundColor && (
+
+
+ Color:{' '}
+
+ {' '}
+ {agent.agentType}{' '}
+
+
+
+ )}
+
+ {!isBuiltInAgent(agent) && (
+ <>
+
+
+ System prompt:
+
+
+
+ {agent.getSystemPrompt()}
+
+ >
+ )}
+
+ )
}
diff --git a/src/components/agents/AgentEditor.tsx b/src/components/agents/AgentEditor.tsx
index e406cf5b2..e5c7b1847 100644
--- a/src/components/agents/AgentEditor.tsx
+++ b/src/components/agents/AgentEditor.tsx
@@ -1,177 +1,246 @@
-import chalk from 'chalk';
-import figures from 'figures';
-import * as React from 'react';
-import { useCallback, useMemo, useState } from 'react';
-import { useSetAppState } from 'src/state/AppState.js';
-import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
-import { Box, Text } from '../../ink.js';
-import { useKeybinding } from '../../keybindings/useKeybinding.js';
-import type { Tools } from '../../Tool.js';
-import { type AgentColorName, setAgentColor } from '../../tools/AgentTool/agentColorManager.js';
-import { type AgentDefinition, getActiveAgentsFromList, isCustomAgent, isPluginAgent } from '../../tools/AgentTool/loadAgentsDir.js';
-import { editFileInEditor } from '../../utils/promptEditor.js';
-import { getActualAgentFilePath, updateAgentFile } from './agentFileUtils.js';
-import { ColorPicker } from './ColorPicker.js';
-import { ModelSelector } from './ModelSelector.js';
-import { ToolSelector } from './ToolSelector.js';
-import { getAgentSourceDisplayName } from './utils.js';
+import chalk from 'chalk'
+import figures from 'figures'
+import * as React from 'react'
+import { useCallback, useMemo, useState } from 'react'
+import { useSetAppState } from 'src/state/AppState.js'
+import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
+import { Box, Text } from '../../ink.js'
+import { useKeybinding } from '../../keybindings/useKeybinding.js'
+import type { Tools } from '../../Tool.js'
+import {
+ type AgentColorName,
+ setAgentColor,
+} from '../../tools/AgentTool/agentColorManager.js'
+import {
+ type AgentDefinition,
+ getActiveAgentsFromList,
+ isCustomAgent,
+ isPluginAgent,
+} from '../../tools/AgentTool/loadAgentsDir.js'
+import { editFileInEditor } from '../../utils/promptEditor.js'
+import { getActualAgentFilePath, updateAgentFile } from './agentFileUtils.js'
+import { ColorPicker } from './ColorPicker.js'
+import { ModelSelector } from './ModelSelector.js'
+import { ToolSelector } from './ToolSelector.js'
+import { getAgentSourceDisplayName } from './utils.js'
+
type Props = {
- agent: AgentDefinition;
- tools: Tools;
- onSaved: (message: string) => void;
- onBack: () => void;
-};
-type EditMode = 'menu' | 'edit-tools' | 'edit-color' | 'edit-model';
+ agent: AgentDefinition
+ tools: Tools
+ onSaved: (message: string) => void
+ onBack: () => void
+}
+
+type EditMode = 'menu' | 'edit-tools' | 'edit-color' | 'edit-model'
+
type SaveChanges = {
- tools?: string[];
- color?: AgentColorName;
- model?: string;
-};
+ tools?: string[]
+ color?: AgentColorName
+ model?: string
+}
+
export function AgentEditor({
agent,
tools,
onSaved,
- onBack
+ onBack,
}: Props): React.ReactNode {
- const setAppState = useSetAppState();
- const [editMode, setEditMode] = useState('menu');
- const [selectedMenuIndex, setSelectedMenuIndex] = useState(0);
- const [error, setError] = useState(null);
- const [selectedColor, setSelectedColor] = useState(agent.color as AgentColorName | undefined);
+ const setAppState = useSetAppState()
+ const [editMode, setEditMode] = useState('menu')
+ const [selectedMenuIndex, setSelectedMenuIndex] = useState(0)
+ const [error, setError] = useState(null)
+ const [selectedColor, setSelectedColor] = useState<
+ AgentColorName | undefined
+ >(agent.color as AgentColorName | undefined)
+
const handleOpenInEditor = useCallback(async () => {
- const filePath = getActualAgentFilePath(agent);
- const result = await editFileInEditor(filePath);
+ const filePath = getActualAgentFilePath(agent)
+ const result = await editFileInEditor(filePath)
+
if (result.error) {
- setError(result.error);
+ setError(result.error)
} else {
- onSaved(`Opened ${agent.agentType} in editor. If you made edits, restart to load the latest version.`);
+ onSaved(
+ `Opened ${agent.agentType} in editor. If you made edits, restart to load the latest version.`,
+ )
}
- }, [agent, onSaved]);
- const handleSave = useCallback(async (changes: SaveChanges = {}) => {
- const {
- tools: newTools,
- color: newColor,
- model: newModel
- } = changes;
- const finalColor = newColor ?? selectedColor;
- const hasToolsChanged = newTools !== undefined;
- const hasModelChanged = newModel !== undefined;
- const hasColorChanged = finalColor !== agent.color;
- if (!hasToolsChanged && !hasModelChanged && !hasColorChanged) {
- return false;
- }
- try {
- // Only custom/plugin agents can be edited
- // this is for type safety; the UI shouldn't allow editing otherwise
- if (!isCustomAgent(agent) && !isPluginAgent(agent)) {
- return false;
+ }, [agent, onSaved])
+
+ const handleSave = useCallback(
+ async (changes: SaveChanges = {}) => {
+ const { tools: newTools, color: newColor, model: newModel } = changes
+ const finalColor = newColor ?? selectedColor
+ const hasToolsChanged = newTools !== undefined
+ const hasModelChanged = newModel !== undefined
+ const hasColorChanged = finalColor !== agent.color
+
+ if (!hasToolsChanged && !hasModelChanged && !hasColorChanged) {
+ return false
}
- await updateAgentFile(agent, agent.whenToUse, newTools ?? agent.tools, agent.getSystemPrompt(), finalColor, newModel ?? agent.model);
- if (hasColorChanged && finalColor) {
- setAgentColor(agent.agentType, finalColor);
- }
- setAppState(state => {
- const allAgents = state.agentDefinitions.allAgents.map(a => a.agentType === agent.agentType ? {
- ...a,
- tools: newTools ?? a.tools,
- color: finalColor,
- model: newModel ?? a.model
- } : a);
- return {
- ...state,
- agentDefinitions: {
- ...state.agentDefinitions,
- activeAgents: getActiveAgentsFromList(allAgents),
- allAgents
+
+ try {
+ // Only custom/plugin agents can be edited
+ // this is for type safety; the UI shouldn't allow editing otherwise
+ if (!isCustomAgent(agent) && !isPluginAgent(agent)) {
+ return false
+ }
+
+ await updateAgentFile(
+ agent,
+ agent.whenToUse,
+ newTools ?? agent.tools,
+ agent.getSystemPrompt(),
+ finalColor,
+ newModel ?? agent.model,
+ )
+
+ if (hasColorChanged && finalColor) {
+ setAgentColor(agent.agentType, finalColor)
+ }
+
+ setAppState(state => {
+ const allAgents = state.agentDefinitions.allAgents.map(a =>
+ a.agentType === agent.agentType
+ ? {
+ ...a,
+ tools: newTools ?? a.tools,
+ color: finalColor,
+ model: newModel ?? a.model,
+ }
+ : a,
+ )
+ return {
+ ...state,
+ agentDefinitions: {
+ ...state.agentDefinitions,
+ activeAgents: getActiveAgentsFromList(allAgents),
+ allAgents,
+ },
}
- };
- });
- onSaved(`Updated agent: ${chalk.bold(agent.agentType)}`);
- return true;
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Failed to save agent');
- return false;
- }
- }, [agent, selectedColor, onSaved, setAppState]);
- const menuItems = useMemo(() => [{
- label: 'Open in editor',
- action: handleOpenInEditor
- }, {
- label: 'Edit tools',
- action: () => setEditMode('edit-tools')
- }, {
- label: 'Edit model',
- action: () => setEditMode('edit-model')
- }, {
- label: 'Edit color',
- action: () => setEditMode('edit-color')
- }], [handleOpenInEditor]);
- const handleEscape = useCallback(() => {
- setError(null);
- if (editMode === 'menu') {
- onBack();
- } else {
- setEditMode('menu');
- }
- }, [editMode, onBack]);
- const handleMenuKeyDown = useCallback((e: KeyboardEvent) => {
- if (e.key === 'up') {
- e.preventDefault();
- setSelectedMenuIndex(index => Math.max(0, index - 1));
- } else if (e.key === 'down') {
- e.preventDefault();
- setSelectedMenuIndex(index_0 => Math.min(menuItems.length - 1, index_0 + 1));
- } else if (e.key === 'return') {
- e.preventDefault();
- const selectedItem = menuItems[selectedMenuIndex];
- if (selectedItem) {
- void selectedItem.action();
+ })
+
+ onSaved(`Updated agent: ${chalk.bold(agent.agentType)}`)
+ return true
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to save agent')
+ return false
}
+ },
+ [agent, selectedColor, onSaved, setAppState],
+ )
+
+ const menuItems = useMemo(
+ () => [
+ { label: 'Open in editor', action: handleOpenInEditor },
+ { label: 'Edit tools', action: () => setEditMode('edit-tools') },
+ { label: 'Edit model', action: () => setEditMode('edit-model') },
+ { label: 'Edit color', action: () => setEditMode('edit-color') },
+ ],
+ [handleOpenInEditor],
+ )
+
+ const handleEscape = useCallback(() => {
+ setError(null)
+ if (editMode === 'menu') {
+ onBack()
+ } else {
+ setEditMode('menu')
}
- }, [menuItems, selectedMenuIndex]);
- useKeybinding('confirm:no', handleEscape, {
- context: 'Confirmation'
- });
- const renderMenu = (): React.ReactNode =>
+ }, [editMode, onBack])
+
+ const handleMenuKeyDown = useCallback(
+ (e: KeyboardEvent) => {
+ if (e.key === 'up') {
+ e.preventDefault()
+ setSelectedMenuIndex(index => Math.max(0, index - 1))
+ } else if (e.key === 'down') {
+ e.preventDefault()
+ setSelectedMenuIndex(index => Math.min(menuItems.length - 1, index + 1))
+ } else if (e.key === 'return') {
+ e.preventDefault()
+ const selectedItem = menuItems[selectedMenuIndex]
+ if (selectedItem) {
+ void selectedItem.action()
+ }
+ }
+ },
+ [menuItems, selectedMenuIndex],
+ )
+
+ useKeybinding('confirm:no', handleEscape, { context: 'Confirmation' })
+
+ const renderMenu = (): React.ReactNode => (
+
Source: {getAgentSourceDisplayName(agent.source)}
- {menuItems.map((item, index_1) =>
- {index_1 === selectedMenuIndex ? `${figures.pointer} ` : ' '}
+ {menuItems.map((item, index) => (
+
+ {index === selectedMenuIndex ? `${figures.pointer} ` : ' '}
{item.label}
- )}
+
+ ))}
- {error &&
+ {error && (
+
{error}
- }
- ;
+
+ )}
+
+ )
+
switch (editMode) {
case 'menu':
- return renderMenu();
+ return renderMenu()
+
case 'edit-tools':
- return {
- setEditMode('menu');
- await handleSave({
- tools: finalTools
- });
- }} />;
+ return (
+ {
+ setEditMode('menu')
+ await handleSave({ tools: finalTools })
+ }}
+ />
+ )
+
case 'edit-color':
- return {
- setSelectedColor(color);
- setEditMode('menu');
- await handleSave({
- color
- });
- }} />;
+ return (
+ {
+ setSelectedColor(color)
+ setEditMode('menu')
+ await handleSave({ color })
+ }}
+ />
+ )
+
case 'edit-model':
- return {
- setEditMode('menu');
- await handleSave({
- model
- });
- }} />;
+ return (
+ {
+ setEditMode('menu')
+ await handleSave({ model })
+ }}
+ />
+ )
+
default:
- return null;
+ return null
}
}
diff --git a/src/components/agents/AgentNavigationFooter.tsx b/src/components/agents/AgentNavigationFooter.tsx
index e20f7301d..9c4fa9f76 100644
--- a/src/components/agents/AgentNavigationFooter.tsx
+++ b/src/components/agents/AgentNavigationFooter.tsx
@@ -1,25 +1,23 @@
-import { c as _c } from "react/compiler-runtime";
-import * as React from 'react';
-import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
-import { Box, Text } from '../../ink.js';
+import * as React from 'react'
+import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
+import { Box, Text } from '../../ink.js'
+
type Props = {
- instructions?: string;
-};
-export function AgentNavigationFooter(t0) {
- const $ = _c(2);
- const {
- instructions: t1
- } = t0;
- const instructions = t1 === undefined ? "Press \u2191\u2193 to navigate \xB7 Enter to select \xB7 Esc to go back" : t1;
- const exitState = useExitOnCtrlCDWithKeybindings();
- const t2 = exitState.pending ? `Press ${exitState.keyName} again to exit` : instructions;
- let t3;
- if ($[0] !== t2) {
- t3 = {t2};
- $[0] = t2;
- $[1] = t3;
- } else {
- t3 = $[1];
- }
- return t3;
+ instructions?: string
+}
+
+export function AgentNavigationFooter({
+ instructions = 'Press ↑↓ to navigate · Enter to select · Esc to go back',
+}: Props): React.ReactNode {
+ const exitState = useExitOnCtrlCDWithKeybindings()
+
+ return (
+
+
+ {exitState.pending
+ ? `Press ${exitState.keyName} again to exit`
+ : instructions}
+
+
+ )
}
diff --git a/src/components/agents/AgentsList.tsx b/src/components/agents/AgentsList.tsx
index 2e394fa1d..6eadf1ef7 100644
--- a/src/components/agents/AgentsList.tsx
+++ b/src/components/agents/AgentsList.tsx
@@ -1,439 +1,342 @@
-import { c as _c } from "react/compiler-runtime";
-import figures from 'figures';
-import * as React from 'react';
-import type { SettingSource } from 'src/utils/settings/constants.js';
-import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
-import { Box, Text } from '../../ink.js';
-import type { ResolvedAgent } from '../../tools/AgentTool/agentDisplay.js';
-import { AGENT_SOURCE_GROUPS, compareAgentsByName, getOverrideSourceLabel, resolveAgentModelDisplay } from '../../tools/AgentTool/agentDisplay.js';
-import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js';
-import { count } from '../../utils/array.js';
-import { Dialog } from '../design-system/Dialog.js';
-import { Divider } from '../design-system/Divider.js';
-import { getAgentSourceDisplayName } from './utils.js';
+import figures from 'figures'
+import * as React from 'react'
+import type { SettingSource } from 'src/utils/settings/constants.js'
+import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
+import { Box, Text } from '../../ink.js'
+import type { ResolvedAgent } from '../../tools/AgentTool/agentDisplay.js'
+import {
+ AGENT_SOURCE_GROUPS,
+ compareAgentsByName,
+ getOverrideSourceLabel,
+ resolveAgentModelDisplay,
+} from '../../tools/AgentTool/agentDisplay.js'
+import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js'
+import { count } from '../../utils/array.js'
+import { Dialog } from '../design-system/Dialog.js'
+import { Divider } from '../design-system/Divider.js'
+import { getAgentSourceDisplayName } from './utils.js'
+
type Props = {
- source: SettingSource | 'all' | 'built-in' | 'plugin';
- agents: ResolvedAgent[];
- onBack: () => void;
- onSelect: (agent: AgentDefinition) => void;
- onCreateNew?: () => void;
- changes?: string[];
-};
-export function AgentsList(t0) {
- const $ = _c(96);
- const {
- source,
- agents,
- onBack,
- onSelect,
- onCreateNew,
- changes
- } = t0;
- const [selectedAgent, setSelectedAgent] = React.useState(null);
- const [isCreateNewSelected, setIsCreateNewSelected] = React.useState(true);
- let t1;
- if ($[0] !== agents) {
- t1 = [...agents].sort(compareAgentsByName);
- $[0] = agents;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- const sortedAgents = t1;
- const getOverrideInfo = _temp;
- let t2;
- if ($[2] !== isCreateNewSelected) {
- t2 = () => {isCreateNewSelected ? `${figures.pointer} ` : " "}Create new agent;
- $[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 {isBuiltIn ? "" : isSelected ? `${figures.pointer} ` : " "}{agent_0.agentType}{resolvedModel && {" \xB7 "}{resolvedModel}}{agent_0.memory && {" \xB7 "}{agent_0.memory} memory}{overriddenBy && {" "}{figures.warning} shadowed by {getOverrideSourceLabel(overriddenBy)}};
- };
- $[4] = isCreateNewSelected;
- $[5] = selectedAgent?.agentType;
- $[6] = selectedAgent?.source;
- $[7] = t3;
- } else {
- t3 = $[7];
- }
- const renderAgent = t3;
- let t4;
- if ($[8] !== sortedAgents || $[9] !== source) {
- bb0: {
- const nonBuiltIn = sortedAgents.filter(_temp2);
- if (source === "all") {
- t4 = AGENT_SOURCE_GROUPS.filter(_temp3).flatMap(t5 => {
- const {
- source: groupSource
- } = t5;
- return nonBuiltIn.filter(a_0 => a_0.source === groupSource);
- });
- break bb0;
- }
- t4 = nonBuiltIn;
+ source: SettingSource | 'all' | 'built-in' | 'plugin'
+ agents: ResolvedAgent[]
+ onBack: () => void
+ onSelect: (agent: AgentDefinition) => void
+ onCreateNew?: () => void
+ changes?: string[]
+}
+
+export function AgentsList({
+ source,
+ agents,
+ onBack,
+ onSelect,
+ onCreateNew,
+ changes,
+}: Props): React.ReactNode {
+ const [selectedAgent, setSelectedAgent] =
+ React.useState(null)
+ const [isCreateNewSelected, setIsCreateNewSelected] = React.useState(true)
+
+ // Sort agents alphabetically by name within each source group
+ const sortedAgents = React.useMemo(
+ () => [...agents].sort(compareAgentsByName),
+ [agents],
+ )
+
+ const getOverrideInfo = (agent: ResolvedAgent) => {
+ return {
+ isOverridden: !!agent.overriddenBy,
+ overriddenBy: agent.overriddenBy || null,
}
- $[8] = sortedAgents;
- $[9] = source;
- $[10] = t4;
- } else {
- t4 = $[10];
}
- const selectableAgentsInOrder = t4;
- let t5;
- let t6;
- if ($[11] !== isCreateNewSelected || $[12] !== onCreateNew || $[13] !== selectableAgentsInOrder || $[14] !== selectedAgent) {
- t5 = () => {
- if (!selectedAgent && !isCreateNewSelected && selectableAgentsInOrder.length > 0) {
- if (onCreateNew) {
- setIsCreateNewSelected(true);
- } else {
- setSelectedAgent(selectableAgentsInOrder[0] || null);
- }
- }
- };
- t6 = [selectableAgentsInOrder, selectedAgent, isCreateNewSelected, onCreateNew];
- $[11] = isCreateNewSelected;
- $[12] = onCreateNew;
- $[13] = selectableAgentsInOrder;
- $[14] = selectedAgent;
- $[15] = t5;
- $[16] = t6;
- } else {
- t5 = $[15];
- t6 = $[16];
+
+ const renderCreateNewOption = () => {
+ return (
+
+
+ {isCreateNewSelected ? `${figures.pointer} ` : ' '}
+
+
+ Create new agent
+
+
+ )
}
- React.useEffect(t5, t6);
- let t7;
- if ($[17] !== isCreateNewSelected || $[18] !== onCreateNew || $[19] !== onSelect || $[20] !== selectableAgentsInOrder || $[21] !== selectedAgent) {
- t7 = e => {
- if (e.key === "return") {
- e.preventDefault();
- if (isCreateNewSelected && onCreateNew) {
- onCreateNew();
- } else {
- if (selectedAgent) {
- onSelect(selectedAgent);
- }
- }
- return;
- }
- if (e.key !== "up" && e.key !== "down") {
- return;
- }
- e.preventDefault();
- const hasCreateOption = !!onCreateNew;
- const totalItems = selectableAgentsInOrder.length + (hasCreateOption ? 1 : 0);
- if (totalItems === 0) {
- return;
- }
- let currentPosition = 0;
- if (!isCreateNewSelected && selectedAgent) {
- const agentIndex = selectableAgentsInOrder.findIndex(a_1 => a_1.agentType === selectedAgent.agentType && a_1.source === selectedAgent.source);
- if (agentIndex >= 0) {
- currentPosition = hasCreateOption ? agentIndex + 1 : agentIndex;
- }
- }
- const newPosition = e.key === "up" ? currentPosition === 0 ? totalItems - 1 : currentPosition - 1 : currentPosition === totalItems - 1 ? 0 : currentPosition + 1;
- if (hasCreateOption && newPosition === 0) {
- setIsCreateNewSelected(true);
- setSelectedAgent(null);
- } else {
- const agentIndex_0 = hasCreateOption ? newPosition - 1 : newPosition;
- const newAgent = selectableAgentsInOrder[agentIndex_0];
- if (newAgent) {
- setIsCreateNewSelected(false);
- setSelectedAgent(newAgent);
- }
- }
- };
- $[17] = isCreateNewSelected;
- $[18] = onCreateNew;
- $[19] = onSelect;
- $[20] = selectableAgentsInOrder;
- $[21] = selectedAgent;
- $[22] = t7;
- } else {
- t7 = $[22];
+
+ const renderAgent = (agent: ResolvedAgent) => {
+ const isBuiltIn = agent.source === 'built-in'
+ const isSelected =
+ !isBuiltIn &&
+ !isCreateNewSelected &&
+ selectedAgent?.agentType === agent.agentType &&
+ selectedAgent?.source === agent.source
+
+ const { isOverridden, overriddenBy } = getOverrideInfo(agent)
+ const dimmed = isBuiltIn || isOverridden
+ const textColor = !isBuiltIn && isSelected ? 'suggestion' : undefined
+
+ const resolvedModel = resolveAgentModelDisplay(agent)
+
+ return (
+
+
+ {isBuiltIn ? '' : isSelected ? `${figures.pointer} ` : ' '}
+
+
+ {agent.agentType}
+
+ {resolvedModel && (
+
+ {' · '}
+ {resolvedModel}
+
+ )}
+ {agent.memory && (
+
+ {' · '}
+ {agent.memory} memory
+
+ )}
+ {overriddenBy && (
+
+ {' '}
+ {figures.warning} shadowed by {getOverrideSourceLabel(overriddenBy)}
+
+ )}
+
+ )
}
- const handleKeyDown = t7;
- let t8;
- if ($[23] !== renderAgent || $[24] !== sortedAgents) {
- t8 = t9 => {
- const title = t9 === undefined ? "Built-in (always available):" : t9;
- const builtInAgents = sortedAgents.filter(_temp4);
- return {title}{builtInAgents.map(renderAgent)};
- };
- $[23] = renderAgent;
- $[24] = sortedAgents;
- $[25] = t8;
- } else {
- t8 = $[25];
- }
- const renderBuiltInAgentsSection = t8;
- let t9;
- if ($[26] !== renderAgent) {
- t9 = (title_0, groupAgents) => {
- if (!groupAgents.length) {
- return null;
- }
- const folderPath = groupAgents[0]?.baseDir;
- return {title_0}{folderPath && ({folderPath})}{groupAgents.map(agent_1 => renderAgent(agent_1))};
- };
- $[26] = renderAgent;
- $[27] = t9;
- } else {
- t9 = $[27];
- }
- const renderAgentGroup = t9;
- let t10;
- if ($[28] !== source) {
- t10 = getAgentSourceDisplayName(source);
- $[28] = source;
- $[29] = t10;
- } else {
- t10 = $[29];
- }
- const sourceTitle = t10;
- let T0;
- let T1;
- let t11;
- let t12;
- let t13;
- let t14;
- let t15;
- let t16;
- let t17;
- let t18;
- let t19;
- let t20;
- let t21;
- let t22;
- if ($[30] !== changes || $[31] !== handleKeyDown || $[32] !== onBack || $[33] !== onCreateNew || $[34] !== renderAgent || $[35] !== renderAgentGroup || $[36] !== renderBuiltInAgentsSection || $[37] !== renderCreateNewOption || $[38] !== sortedAgents || $[39] !== source || $[40] !== sourceTitle) {
- t22 = Symbol.for("react.early_return_sentinel");
- bb1: {
- const builtInAgents_0 = sortedAgents.filter(_temp5);
- const hasNoAgents = !sortedAgents.length || source !== "built-in" && !sortedAgents.some(_temp6);
- if (hasNoAgents) {
- let t23;
- if ($[55] !== onCreateNew || $[56] !== renderCreateNewOption) {
- t23 = onCreateNew && {renderCreateNewOption()};
- $[55] = onCreateNew;
- $[56] = renderCreateNewOption;
- $[57] = t23;
- } else {
- t23 = $[57];
- }
- let t24;
- let t25;
- let t26;
- if ($[58] === Symbol.for("react.memo_cache_sentinel")) {
- t24 = No agents found. Create specialized subagents that Claude can delegate to.;
- t25 = Each subagent has its own context window, custom system prompt, and specific tools.;
- t26 = Try creating: Code Reviewer, Code Simplifier, Security Reviewer, Tech Lead, or UX Reviewer.;
- $[58] = t24;
- $[59] = t25;
- $[60] = t26;
- } else {
- t24 = $[58];
- t25 = $[59];
- t26 = $[60];
- }
- let t27;
- if ($[61] !== renderBuiltInAgentsSection || $[62] !== sortedAgents || $[63] !== source) {
- t27 = source !== "built-in" && sortedAgents.some(_temp7) && <>{renderBuiltInAgentsSection()}>;
- $[61] = renderBuiltInAgentsSection;
- $[62] = sortedAgents;
- $[63] = source;
- $[64] = t27;
- } else {
- t27 = $[64];
- }
- let t28;
- if ($[65] !== handleKeyDown || $[66] !== t23 || $[67] !== t27) {
- t28 = {t23}{t24}{t25}{t26}{t27};
- $[65] = handleKeyDown;
- $[66] = t23;
- $[67] = t27;
- $[68] = t28;
- } else {
- t28 = $[68];
- }
- let t29;
- if ($[69] !== onBack || $[70] !== sourceTitle || $[71] !== t28) {
- t29 = ;
- $[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 && {changes[changes.length - 1]};
- $[75] = changes;
- $[76] = t21;
- } else {
- t21 = $[76];
- }
- T0 = Box;
- t11 = "column";
- t12 = 0;
- t13 = true;
- t14 = handleKeyDown;
- if ($[77] !== onCreateNew || $[78] !== renderCreateNewOption) {
- t15 = onCreateNew && {renderCreateNewOption()};
- $[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 {renderAgentGroup(label, sortedAgents.filter(a_7 => a_7.source === groupSource_0))};
- })}{builtInAgents_0.length > 0 && Built-in agents (always available){builtInAgents_0.map(renderAgent)}}> : source === "built-in" ? <>Built-in agents are provided by default and cannot be modified.{sortedAgents.map(agent_2 => renderAgent(agent_2))}> : <>{sortedAgents.filter(_temp0).map(agent_3 => renderAgent(agent_3))}{sortedAgents.some(_temp1) && <>{renderBuiltInAgentsSection()}>}>;
+
+ const selectableAgentsInOrder = React.useMemo(() => {
+ const nonBuiltIn = sortedAgents.filter(a => a.source !== 'built-in')
+ if (source === 'all') {
+ return AGENT_SOURCE_GROUPS.filter(g => g.source !== 'built-in').flatMap(
+ ({ source: groupSource }) =>
+ nonBuiltIn.filter(a => a.source === groupSource),
+ )
+ }
+ return nonBuiltIn
+ }, [sortedAgents, source])
+
+ // Set initial selection
+ React.useEffect(() => {
+ if (
+ !selectedAgent &&
+ !isCreateNewSelected &&
+ selectableAgentsInOrder.length > 0
+ ) {
+ if (onCreateNew) {
+ setIsCreateNewSelected(true)
+ } else {
+ setSelectedAgent(selectableAgentsInOrder[0] || null)
+ }
+ }
+ }, [selectableAgentsInOrder, selectedAgent, isCreateNewSelected, onCreateNew])
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'return') {
+ e.preventDefault()
+ if (isCreateNewSelected && onCreateNew) {
+ onCreateNew()
+ } else if (selectedAgent) {
+ onSelect(selectedAgent)
+ }
+ return
+ }
+
+ if (e.key !== 'up' && e.key !== 'down') return
+ e.preventDefault()
+
+ // Handle navigation with "Create New Agent" option
+ const hasCreateOption = !!onCreateNew
+ const totalItems =
+ selectableAgentsInOrder.length + (hasCreateOption ? 1 : 0)
+
+ if (totalItems === 0) return
+
+ // Calculate current position in list (0 = create new, 1+ = agents)
+ let currentPosition = 0
+ if (!isCreateNewSelected && selectedAgent) {
+ const agentIndex = selectableAgentsInOrder.findIndex(
+ a =>
+ a.agentType === selectedAgent.agentType &&
+ a.source === selectedAgent.source,
+ )
+ if (agentIndex >= 0) {
+ currentPosition = hasCreateOption ? agentIndex + 1 : agentIndex
+ }
+ }
+
+ // Calculate new position with wrap-around
+ const newPosition =
+ e.key === 'up'
+ ? currentPosition === 0
+ ? totalItems - 1
+ : currentPosition - 1
+ : currentPosition === totalItems - 1
+ ? 0
+ : currentPosition + 1
+
+ // Update selection based on new position
+ if (hasCreateOption && newPosition === 0) {
+ setIsCreateNewSelected(true)
+ setSelectedAgent(null)
+ } else {
+ const agentIndex = hasCreateOption ? newPosition - 1 : newPosition
+ const newAgent = selectableAgentsInOrder[agentIndex]
+ if (newAgent) {
+ setIsCreateNewSelected(false)
+ setSelectedAgent(newAgent)
+ }
}
- $[30] = changes;
- $[31] = handleKeyDown;
- $[32] = onBack;
- $[33] = onCreateNew;
- $[34] = renderAgent;
- $[35] = renderAgentGroup;
- $[36] = renderBuiltInAgentsSection;
- $[37] = renderCreateNewOption;
- $[38] = sortedAgents;
- $[39] = source;
- $[40] = sourceTitle;
- $[41] = T0;
- $[42] = T1;
- $[43] = t11;
- $[44] = t12;
- $[45] = t13;
- $[46] = t14;
- $[47] = t15;
- $[48] = t16;
- $[49] = t17;
- $[50] = t18;
- $[51] = t19;
- $[52] = t20;
- $[53] = t21;
- $[54] = t22;
- } else {
- T0 = $[41];
- T1 = $[42];
- t11 = $[43];
- t12 = $[44];
- t13 = $[45];
- t14 = $[46];
- t15 = $[47];
- t16 = $[48];
- t17 = $[49];
- t18 = $[50];
- t19 = $[51];
- t20 = $[52];
- t21 = $[53];
- t22 = $[54];
}
- if (t22 !== Symbol.for("react.early_return_sentinel")) {
- return t22;
+
+ const renderBuiltInAgentsSection = (
+ title = 'Built-in (always available):',
+ ) => {
+ const builtInAgents = sortedAgents.filter(a => a.source === 'built-in')
+ return (
+
+
+ {title}
+
+ {builtInAgents.map(renderAgent)}
+
+ )
}
- let t23;
- if ($[80] !== T0 || $[81] !== t11 || $[82] !== t12 || $[83] !== t13 || $[84] !== t14 || $[85] !== t15 || $[86] !== t16) {
- t23 = {t15}{t16};
- $[80] = T0;
- $[81] = t11;
- $[82] = t12;
- $[83] = t13;
- $[84] = t14;
- $[85] = t15;
- $[86] = t16;
- $[87] = t23;
- } else {
- t23 = $[87];
+
+ const renderAgentGroup = (title: string, groupAgents: ResolvedAgent[]) => {
+ if (!groupAgents.length) return null
+
+ const folderPath = groupAgents[0]?.baseDir
+
+ return (
+
+
+
+ {title}
+
+ {folderPath && ({folderPath})}
+
+ {groupAgents.map(agent => renderAgent(agent))}
+
+ )
}
- let t24;
- if ($[88] !== T1 || $[89] !== t17 || $[90] !== t18 || $[91] !== t19 || $[92] !== t20 || $[93] !== t21 || $[94] !== t23) {
- t24 = {t21}{t23};
- $[88] = T1;
- $[89] = t17;
- $[90] = t18;
- $[91] = t19;
- $[92] = t20;
- $[93] = t21;
- $[94] = t23;
- $[95] = t24;
- } else {
- t24 = $[95];
+
+ const sourceTitle = getAgentSourceDisplayName(source)
+
+ const builtInAgents = sortedAgents.filter(a => a.source === 'built-in')
+
+ const hasNoAgents =
+ !sortedAgents.length ||
+ (source !== 'built-in' && !sortedAgents.some(a => a.source !== 'built-in'))
+
+ if (hasNoAgents) {
+ return (
+
+ )
}
- return t24;
-}
-function _temp1(a_9) {
- return a_9.source === "built-in";
-}
-function _temp0(a_8) {
- return a_8.source !== "built-in";
-}
-function _temp9(g_0) {
- return g_0.source !== "built-in";
-}
-function _temp8(a_6) {
- return !a_6.overriddenBy;
-}
-function _temp7(a_5) {
- return a_5.source === "built-in";
-}
-function _temp6(a_4) {
- return a_4.source !== "built-in";
-}
-function _temp5(a_3) {
- return a_3.source === "built-in";
-}
-function _temp4(a_2) {
- return a_2.source === "built-in";
-}
-function _temp3(g) {
- return g.source !== "built-in";
-}
-function _temp2(a) {
- return a.source !== "built-in";
-}
-function _temp(agent) {
- return {
- isOverridden: !!agent.overriddenBy,
- overriddenBy: agent.overriddenBy || null
- };
+
+ return (
+
+ )
}
diff --git a/src/components/agents/AgentsMenu.tsx b/src/components/agents/AgentsMenu.tsx
index 5a3f56eed..91de932b4 100644
--- a/src/components/agents/AgentsMenu.tsx
+++ b/src/components/agents/AgentsMenu.tsx
@@ -1,799 +1,369 @@
-import { c as _c } from "react/compiler-runtime";
-import chalk from 'chalk';
-import * as React from 'react';
-import { useCallback, useMemo, useState } from 'react';
-import type { SettingSource } from 'src/utils/settings/constants.js';
-import type { CommandResultDisplay } from '../../commands.js';
-import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
-import { useMergedTools } from '../../hooks/useMergedTools.js';
-import { Box, Text } from '../../ink.js';
-import { useAppState, useSetAppState } from '../../state/AppState.js';
-import type { Tools } from '../../Tool.js';
-import { type ResolvedAgent, resolveAgentOverrides } from '../../tools/AgentTool/agentDisplay.js';
-import { type AgentDefinition, getActiveAgentsFromList } from '../../tools/AgentTool/loadAgentsDir.js';
-import { toError } from '../../utils/errors.js';
-import { logError } from '../../utils/log.js';
-import { Select } from '../CustomSelect/select.js';
-import { Dialog } from '../design-system/Dialog.js';
-import { AgentDetail } from './AgentDetail.js';
-import { AgentEditor } from './AgentEditor.js';
-import { AgentNavigationFooter } from './AgentNavigationFooter.js';
-import { AgentsList } from './AgentsList.js';
-import { deleteAgentFromFile } from './agentFileUtils.js';
-import { CreateAgentWizard } from './new-agent-creation/CreateAgentWizard.js';
-import type { ModeState } from './types.js';
+import chalk from 'chalk'
+import * as React from 'react'
+import { useCallback, useMemo, useState } from 'react'
+import type { SettingSource } from 'src/utils/settings/constants.js'
+import type { CommandResultDisplay } from '../../commands.js'
+import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
+import { useMergedTools } from '../../hooks/useMergedTools.js'
+import { Box, Text } from '../../ink.js'
+import { useAppState, useSetAppState } from '../../state/AppState.js'
+import type { Tools } from '../../Tool.js'
+import {
+ type ResolvedAgent,
+ resolveAgentOverrides,
+} from '../../tools/AgentTool/agentDisplay.js'
+import {
+ type AgentDefinition,
+ getActiveAgentsFromList,
+} from '../../tools/AgentTool/loadAgentsDir.js'
+import { toError } from '../../utils/errors.js'
+import { logError } from '../../utils/log.js'
+import { Select } from '../CustomSelect/select.js'
+import { Dialog } from '../design-system/Dialog.js'
+import { AgentDetail } from './AgentDetail.js'
+import { AgentEditor } from './AgentEditor.js'
+import { AgentNavigationFooter } from './AgentNavigationFooter.js'
+import { AgentsList } from './AgentsList.js'
+import { deleteAgentFromFile } from './agentFileUtils.js'
+import { CreateAgentWizard } from './new-agent-creation/CreateAgentWizard.js'
+import type { ModeState } from './types.js'
+
type Props = {
- tools: Tools;
- onExit: (result?: string, options?: {
- display?: CommandResultDisplay;
- }) => void;
-};
-export function AgentsMenu(t0) {
- const $ = _c(157);
- const {
- tools,
- onExit
- } = t0;
- let t1;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = {
- mode: "list-agents",
- source: "all"
- };
- $[0] = t1;
- } else {
- t1 = $[0];
- }
- const [modeState, setModeState] = useState(t1);
- const agentDefinitions = useAppState(_temp);
- const mcpTools = useAppState(_temp2);
- const toolPermissionContext = useAppState(_temp3);
- const setAppState = useSetAppState();
- const {
- allAgents,
- activeAgents: agents
- } = agentDefinitions;
- let t2;
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
- t2 = [];
- $[1] = t2;
- } else {
- t2 = $[1];
- }
- const [changes, setChanges] = useState(t2);
- const mergedTools = useMergedTools(tools, mcpTools, toolPermissionContext);
- useExitOnCtrlCDWithKeybindings();
- let t3;
- if ($[2] !== allAgents) {
- t3 = allAgents.filter(_temp4);
- $[2] = allAgents;
- $[3] = t3;
- } else {
- t3 = $[3];
- }
- let t4;
- if ($[4] !== allAgents) {
- t4 = allAgents.filter(_temp5);
- $[4] = allAgents;
- $[5] = t4;
- } else {
- t4 = $[5];
- }
- let t5;
- if ($[6] !== allAgents) {
- t5 = allAgents.filter(_temp6);
- $[6] = allAgents;
- $[7] = t5;
- } else {
- t5 = $[7];
- }
- let t6;
- if ($[8] !== allAgents) {
- t6 = allAgents.filter(_temp7);
- $[8] = allAgents;
- $[9] = t6;
- } else {
- t6 = $[9];
- }
- let t7;
- if ($[10] !== allAgents) {
- t7 = allAgents.filter(_temp8);
- $[10] = allAgents;
- $[11] = t7;
- } else {
- t7 = $[11];
- }
- let t8;
- if ($[12] !== allAgents) {
- t8 = allAgents.filter(_temp9);
- $[12] = allAgents;
- $[13] = t8;
- } else {
- t8 = $[13];
- }
- let t9;
- if ($[14] !== allAgents) {
- t9 = allAgents.filter(_temp0);
- $[14] = allAgents;
- $[15] = t9;
- } else {
- t9 = $[15];
- }
- let t10;
- if ($[16] !== allAgents || $[17] !== t3 || $[18] !== t4 || $[19] !== t5 || $[20] !== t6 || $[21] !== t7 || $[22] !== t8 || $[23] !== t9) {
- t10 = {
- "built-in": t3,
- userSettings: t4,
- projectSettings: t5,
- policySettings: t6,
- localSettings: t7,
- flagSettings: t8,
- plugin: t9,
- all: allAgents
- };
- $[16] = allAgents;
- $[17] = t3;
- $[18] = t4;
- $[19] = t5;
- $[20] = t6;
- $[21] = t7;
- $[22] = t8;
- $[23] = t9;
- $[24] = t10;
- } else {
- t10 = $[24];
- }
- const agentsBySource = t10;
- let t11;
- if ($[25] === Symbol.for("react.memo_cache_sentinel")) {
- t11 = message => {
- setChanges(prev => [...prev, message]);
- setModeState({
- mode: "list-agents",
- source: "all"
- });
- };
- $[25] = t11;
- } else {
- t11 = $[25];
- }
- const handleAgentCreated = t11;
- let t12;
- if ($[26] !== setAppState) {
- t12 = async agent => {
- ;
+ tools: Tools
+ onExit: (
+ result?: string,
+ options?: { display?: CommandResultDisplay },
+ ) => void
+}
+
+export function AgentsMenu({ tools, onExit }: Props): React.ReactNode {
+ const [modeState, setModeState] = useState({
+ mode: 'list-agents',
+ source: 'all',
+ })
+ const agentDefinitions = useAppState(s => s.agentDefinitions)
+ const mcpTools = useAppState(s => s.mcp.tools)
+ const toolPermissionContext = useAppState(s => s.toolPermissionContext)
+ const setAppState = useSetAppState()
+ const { allAgents, activeAgents: agents } = agentDefinitions
+ const [changes, setChanges] = useState([])
+
+ // Get MCP tools from app state and merge with local tools
+ const mergedTools = useMergedTools(tools, mcpTools, toolPermissionContext)
+
+ useExitOnCtrlCDWithKeybindings()
+
+ const agentsBySource: Record<
+ SettingSource | 'all' | 'built-in' | 'plugin',
+ AgentDefinition[]
+ > = useMemo(
+ () => ({
+ 'built-in': allAgents.filter(a => a.source === 'built-in'),
+ userSettings: allAgents.filter(a => a.source === 'userSettings'),
+ projectSettings: allAgents.filter(a => a.source === 'projectSettings'),
+ policySettings: allAgents.filter(a => a.source === 'policySettings'),
+ localSettings: allAgents.filter(a => a.source === 'localSettings'),
+ flagSettings: allAgents.filter(a => a.source === 'flagSettings'),
+ plugin: allAgents.filter(a => a.source === 'plugin'),
+ all: allAgents,
+ }),
+ [allAgents],
+ )
+
+ const handleAgentCreated = useCallback((message: string) => {
+ setChanges(prev => [...prev, message])
+ setModeState({ mode: 'list-agents', source: 'all' })
+ }, [])
+
+ const handleAgentDeleted = useCallback(
+ async (agent: AgentDefinition) => {
try {
- await deleteAgentFromFile(agent);
+ await deleteAgentFromFile(agent)
setAppState(state => {
- const allAgents_0 = state.agentDefinitions.allAgents.filter(a_6 => !(a_6.agentType === agent.agentType && a_6.source === agent.source));
+ const allAgents = state.agentDefinitions.allAgents.filter(
+ a =>
+ !(a.agentType === agent.agentType && a.source === agent.source),
+ )
return {
...state,
agentDefinitions: {
...state.agentDefinitions,
- allAgents: allAgents_0,
- activeAgents: getActiveAgentsFromList(allAgents_0)
- }
- };
- });
- setChanges(prev_0 => [...prev_0, `Deleted agent: ${chalk.bold(agent.agentType)}`]);
- setModeState({
- mode: "list-agents",
- source: "all"
- });
- } catch (t13) {
- const error = t13;
- logError(toError(error));
+ allAgents,
+ activeAgents: getActiveAgentsFromList(allAgents),
+ },
+ }
+ })
+
+ setChanges(prev => [
+ ...prev,
+ `Deleted agent: ${chalk.bold(agent.agentType)}`,
+ ])
+ // Go back to the agents list after deletion
+ setModeState({ mode: 'list-agents', source: 'all' })
+ } catch (error) {
+ logError(toError(error))
}
- };
- $[26] = setAppState;
- $[27] = t12;
- } else {
- t12 = $[27];
- }
- const handleAgentDeleted = t12;
+ },
+ [setAppState],
+ )
+
+ // Render based on mode
switch (modeState.mode) {
- case "list-agents":
- {
- let t13;
- if ($[28] !== agentsBySource || $[29] !== modeState.source) {
- t13 = modeState.source === "all" ? [...agentsBySource["built-in"], ...agentsBySource.userSettings, ...agentsBySource.projectSettings, ...agentsBySource.localSettings, ...agentsBySource.policySettings, ...agentsBySource.flagSettings, ...agentsBySource.plugin] : agentsBySource[modeState.source];
- $[28] = agentsBySource;
- $[29] = modeState.source;
- $[30] = t13;
- } else {
- t13 = $[30];
- }
- const agentsToShow = t13;
- let t14;
- if ($[31] !== agents || $[32] !== agentsToShow) {
- t14 = resolveAgentOverrides(agentsToShow, agents);
- $[31] = agents;
- $[32] = agentsToShow;
- $[33] = t14;
- } else {
- t14 = $[33];
- }
- const allResolved = t14;
- const resolvedAgents = allResolved;
- let t15;
- if ($[34] !== changes || $[35] !== onExit) {
- t15 = () => {
- const exitMessage = changes.length > 0 ? `Agent changes:\n${changes.join("\n")}` : undefined;
- onExit(exitMessage ?? "Agents dialog dismissed", {
- display: changes.length === 0 ? "system" : undefined
- });
- };
- $[34] = changes;
- $[35] = onExit;
- $[36] = t15;
- } else {
- t15 = $[36];
- }
- let t16;
- if ($[37] !== modeState) {
- t16 = agent_0 => setModeState({
- mode: "agent-menu",
- agent: agent_0,
- previousMode: modeState
- });
- $[37] = modeState;
- $[38] = t16;
- } else {
- t16 = $[38];
- }
- let t17;
- if ($[39] === Symbol.for("react.memo_cache_sentinel")) {
- t17 = () => setModeState({
- mode: "create-agent"
- });
- $[39] = t17;
- } else {
- t17 = $[39];
- }
- let t18;
- if ($[40] !== changes || $[41] !== modeState.source || $[42] !== resolvedAgents || $[43] !== t15 || $[44] !== t16) {
- t18 = ;
- $[40] = changes;
- $[41] = modeState.source;
- $[42] = resolvedAgents;
- $[43] = t15;
- $[44] = t16;
- $[45] = t18;
- } else {
- t18 = $[45];
- }
- let t19;
- if ($[46] === Symbol.for("react.memo_cache_sentinel")) {
- t19 = ;
- $[46] = t19;
- } else {
- t19 = $[46];
- }
- let t20;
- if ($[47] !== t18) {
- t20 = <>{t18}{t19}>;
- $[47] = t18;
- $[48] = t20;
- } else {
- t20 = $[48];
- }
- return t20;
- }
- case "create-agent":
- {
- let t13;
- if ($[49] === Symbol.for("react.memo_cache_sentinel")) {
- t13 = () => setModeState({
- mode: "list-agents",
- source: "all"
- });
- $[49] = t13;
- } else {
- t13 = $[49];
- }
- let t14;
- if ($[50] !== agents || $[51] !== mergedTools) {
- t14 = ;
- $[50] = agents;
- $[51] = mergedTools;
- $[52] = t14;
- } else {
- t14 = $[52];
- }
- return t14;
- }
- case "agent-menu":
- {
- let t13;
- if ($[53] !== allAgents || $[54] !== modeState.agent.agentType || $[55] !== modeState.agent.source) {
- let t14;
- if ($[57] !== modeState.agent.agentType || $[58] !== modeState.agent.source) {
- t14 = a_9 => a_9.agentType === modeState.agent.agentType && a_9.source === modeState.agent.source;
- $[57] = modeState.agent.agentType;
- $[58] = modeState.agent.source;
- $[59] = t14;
- } else {
- t14 = $[59];
- }
- t13 = allAgents.find(t14);
- $[53] = allAgents;
- $[54] = modeState.agent.agentType;
- $[55] = modeState.agent.source;
- $[56] = t13;
- } else {
- t13 = $[56];
- }
- const freshAgent_1 = t13;
- const agentToUse = freshAgent_1 || modeState.agent;
- const isEditable = agentToUse.source !== "built-in" && agentToUse.source !== "plugin" && agentToUse.source !== "flagSettings";
- let t14;
- if ($[60] === Symbol.for("react.memo_cache_sentinel")) {
- t14 = {
- label: "View agent",
- value: "view"
- };
- $[60] = t14;
- } else {
- t14 = $[60];
- }
- let t15;
- if ($[61] !== isEditable) {
- t15 = isEditable ? [{
- label: "Edit agent",
- value: "edit"
- }, {
- label: "Delete agent",
- value: "delete"
- }] : [];
- $[61] = isEditable;
- $[62] = t15;
- } else {
- t15 = $[62];
- }
- let t16;
- if ($[63] === Symbol.for("react.memo_cache_sentinel")) {
- t16 = {
- label: "Back",
- value: "back"
- };
- $[63] = t16;
- } else {
- t16 = $[63];
- }
- let t17;
- if ($[64] !== t15) {
- t17 = [t14, ...t15, t16];
- $[64] = t15;
- $[65] = t17;
- } else {
- t17 = $[65];
- }
- const menuItems = t17;
- let t18;
- if ($[66] !== agentToUse || $[67] !== modeState) {
- t18 = value_0 => {
- bb129: switch (value_0) {
- case "view":
- {
- setModeState({
- mode: "view-agent",
- agent: agentToUse,
- previousMode: modeState.previousMode
- });
- break bb129;
- }
- case "edit":
- {
- setModeState({
- mode: "edit-agent",
- agent: agentToUse,
- previousMode: modeState
- });
- break bb129;
- }
- case "delete":
- {
- setModeState({
- mode: "delete-confirm",
- agent: agentToUse,
- previousMode: modeState
- });
- break bb129;
- }
- case "back":
- {
- setModeState(modeState.previousMode);
- }
+ case 'list-agents': {
+ const agentsToShow =
+ modeState.source === 'all'
+ ? [
+ ...agentsBySource['built-in'],
+ ...agentsBySource['userSettings'],
+ ...agentsBySource['projectSettings'],
+ ...agentsBySource['localSettings'],
+ ...agentsBySource['policySettings'],
+ ...agentsBySource['flagSettings'],
+ ...agentsBySource['plugin'],
+ ]
+ : agentsBySource[modeState.source]
+
+ // Resolve overrides and filter to the agents we want to show
+ const allResolved = resolveAgentOverrides(agentsToShow, agents)
+ const resolvedAgents: ResolvedAgent[] = allResolved
+
+ return (
+ <>
+ {
+ const exitMessage =
+ changes.length > 0
+ ? `Agent changes:\n${changes.join('\n')}`
+ : undefined
+ onExit(exitMessage ?? 'Agents dialog dismissed', {
+ display: changes.length === 0 ? 'system' : undefined,
+ })
+ }}
+ onSelect={agent =>
+ setModeState({
+ mode: 'agent-menu',
+ agent,
+ previousMode: modeState,
+ })
}
- };
- $[66] = agentToUse;
- $[67] = modeState;
- $[68] = t18;
- } else {
- t18 = $[68];
+ onCreateNew={() => setModeState({ mode: 'create-agent' })}
+ changes={changes}
+ />
+
+ >
+ )
+ }
+
+ case 'create-agent':
+ return (
+ setModeState({ mode: 'list-agents', source: 'all' })}
+ />
+ )
+
+ case 'agent-menu': {
+ // Always use fresh agent data
+ const freshAgent = allAgents.find(
+ a =>
+ a.agentType === modeState.agent.agentType &&
+ a.source === modeState.agent.source,
+ )
+ const agentToUse = freshAgent || modeState.agent
+
+ const isEditable =
+ agentToUse.source !== 'built-in' &&
+ agentToUse.source !== 'plugin' &&
+ agentToUse.source !== 'flagSettings'
+ const menuItems = [
+ { label: 'View agent', value: 'view' },
+ ...(isEditable
+ ? [
+ { label: 'Edit agent', value: 'edit' },
+ { label: 'Delete agent', value: 'delete' },
+ ]
+ : []),
+ { label: 'Back', value: 'back' },
+ ]
+
+ const handleMenuSelect = (value: string): void => {
+ switch (value) {
+ case 'view':
+ setModeState({
+ mode: 'view-agent',
+ agent: agentToUse,
+ previousMode: modeState.previousMode,
+ })
+ break
+ case 'edit':
+ setModeState({
+ mode: 'edit-agent',
+ agent: agentToUse,
+ previousMode: modeState,
+ })
+ break
+ case 'delete':
+ setModeState({
+ mode: 'delete-confirm',
+ agent: agentToUse,
+ previousMode: modeState,
+ })
+ break
+ case 'back':
+ setModeState(modeState.previousMode)
+ break
}
- const handleMenuSelect = t18;
- let t19;
- if ($[69] !== modeState.previousMode) {
- t19 = () => setModeState(modeState.previousMode);
- $[69] = modeState.previousMode;
- $[70] = t19;
- } else {
- t19 = $[70];
- }
- let t20;
- if ($[71] !== modeState.previousMode) {
- t20 = () => setModeState(modeState.previousMode);
- $[71] = modeState.previousMode;
- $[72] = t20;
- } else {
- t20 = $[72];
- }
- let t21;
- if ($[73] !== handleMenuSelect || $[74] !== menuItems || $[75] !== t20) {
- t21 = ;
- $[73] = handleMenuSelect;
- $[74] = menuItems;
- $[75] = t20;
- $[76] = t21;
- } else {
- t21 = $[76];
- }
- let t22;
- if ($[77] !== changes) {
- t22 = changes.length > 0 && {changes[changes.length - 1]};
- $[77] = changes;
- $[78] = t22;
- } else {
- t22 = $[78];
- }
- let t23;
- if ($[79] !== t21 || $[80] !== t22) {
- t23 = {t21}{t22};
- $[79] = t21;
- $[80] = t22;
- $[81] = t23;
- } else {
- t23 = $[81];
- }
- let t24;
- if ($[82] !== modeState.agent.agentType || $[83] !== t19 || $[84] !== t23) {
- t24 = ;
- $[82] = modeState.agent.agentType;
- $[83] = t19;
- $[84] = t23;
- $[85] = t24;
- } else {
- t24 = $[85];
- }
- let t25;
- if ($[86] === Symbol.for("react.memo_cache_sentinel")) {
- t25 = ;
- $[86] = t25;
- } else {
- t25 = $[86];
- }
- let t26;
- if ($[87] !== t24) {
- t26 = <>{t24}{t25}>;
- $[87] = t24;
- $[88] = t26;
- } else {
- t26 = $[88];
- }
- return t26;
}
- case "view-agent":
- {
- let t13;
- if ($[89] !== allAgents || $[90] !== modeState.agent) {
- let t14;
- if ($[92] !== modeState.agent) {
- t14 = a_8 => a_8.agentType === modeState.agent.agentType && a_8.source === modeState.agent.source;
- $[92] = modeState.agent;
- $[93] = t14;
- } else {
- t14 = $[93];
- }
- t13 = allAgents.find(t14);
- $[89] = allAgents;
- $[90] = modeState.agent;
- $[91] = t13;
- } else {
- t13 = $[91];
- }
- const freshAgent_0 = t13;
- const agentToDisplay = freshAgent_0 || modeState.agent;
- let t14;
- if ($[94] !== agentToDisplay || $[95] !== modeState.previousMode) {
- t14 = () => setModeState({
- mode: "agent-menu",
- agent: agentToDisplay,
- previousMode: modeState.previousMode
- });
- $[94] = agentToDisplay;
- $[95] = modeState.previousMode;
- $[96] = t14;
- } else {
- t14 = $[96];
- }
- let t15;
- if ($[97] !== agentToDisplay || $[98] !== modeState.previousMode) {
- t15 = () => setModeState({
- mode: "agent-menu",
- agent: agentToDisplay,
- previousMode: modeState.previousMode
- });
- $[97] = agentToDisplay;
- $[98] = modeState.previousMode;
- $[99] = t15;
- } else {
- t15 = $[99];
- }
- let t16;
- if ($[100] !== agentToDisplay || $[101] !== allAgents || $[102] !== mergedTools || $[103] !== t15) {
- t16 = ;
- $[100] = agentToDisplay;
- $[101] = allAgents;
- $[102] = mergedTools;
- $[103] = t15;
- $[104] = t16;
- } else {
- t16 = $[104];
- }
- let t17;
- if ($[105] !== agentToDisplay.agentType || $[106] !== t14 || $[107] !== t16) {
- t17 = ;
- $[105] = agentToDisplay.agentType;
- $[106] = t14;
- $[107] = t16;
- $[108] = t17;
- } else {
- t17 = $[108];
- }
- let t18;
- if ($[109] === Symbol.for("react.memo_cache_sentinel")) {
- t18 = ;
- $[109] = t18;
- } else {
- t18 = $[109];
- }
- let t19;
- if ($[110] !== t17) {
- t19 = <>{t17}{t18}>;
- $[110] = t17;
- $[111] = t19;
- } else {
- t19 = $[111];
- }
- return t19;
- }
- case "delete-confirm":
- {
- let t13;
- if ($[112] === Symbol.for("react.memo_cache_sentinel")) {
- t13 = [{
- label: "Yes, delete",
- value: "yes"
- }, {
- label: "No, cancel",
- value: "no"
- }];
- $[112] = t13;
- } else {
- t13 = $[112];
- }
- const deleteOptions = t13;
- let t14;
- if ($[113] !== modeState) {
- t14 = () => {
- if ("previousMode" in modeState) {
- setModeState(modeState.previousMode);
+
+ return (
+ <>
+
+
+ >
+ )
+ }
+
+ case 'view-agent': {
+ // Always use fresh agent data from allAgents
+ const freshAgent = allAgents.find(
+ a =>
+ a.agentType === modeState.agent.agentType &&
+ a.source === modeState.agent.source,
+ )
+ const agentToDisplay = freshAgent || modeState.agent
+
+ return (
+ <>
+
+
+ >
+ )
+ }
+
+ case 'delete-confirm': {
+ const deleteOptions = [
+ { label: 'Yes, delete', value: 'yes' },
+ { label: 'No, cancel', value: 'no' },
+ ]
+
+ return (
+ <>
+ {
+ if ('previousMode' in modeState)
+ setModeState(modeState.previousMode)
+ }}
+ color="error"
+ >
+
+ Are you sure you want to delete the agent{' '}
+ {modeState.agent.agentType}?
+
+
+ Source: {modeState.agent.source}
+
+
+
+
+
+ >
+ )
+ }
+
+ case 'edit-agent': {
+ // Always use fresh agent data
+ const freshAgent = allAgents.find(
+ a =>
+ a.agentType === modeState.agent.agentType &&
+ a.source === modeState.agent.source,
+ )
+ const agentToEdit = freshAgent || modeState.agent
+
+ return (
+ <>
+ setModeState(modeState.previousMode)}
+ hideInputGuide
+ >
+ {
+ handleAgentCreated(message)
+ setModeState(modeState.previousMode)
+ }}
+ onBack={() => setModeState(modeState.previousMode)}
+ />
+
+
+ >
+ )
+ }
+
default:
- {
- return null;
- }
+ return null
}
}
-function _temp0(a_5) {
- return a_5.source === "plugin";
-}
-function _temp9(a_4) {
- return a_4.source === "flagSettings";
-}
-function _temp8(a_3) {
- return a_3.source === "localSettings";
-}
-function _temp7(a_2) {
- return a_2.source === "policySettings";
-}
-function _temp6(a_1) {
- return a_1.source === "projectSettings";
-}
-function _temp5(a_0) {
- return a_0.source === "userSettings";
-}
-function _temp4(a) {
- return a.source === "built-in";
-}
-function _temp3(s_1) {
- return s_1.toolPermissionContext;
-}
-function _temp2(s_0) {
- return s_0.mcp.tools;
-}
-function _temp(s) {
- return s.agentDefinitions;
-}
diff --git a/src/components/agents/ColorPicker.tsx b/src/components/agents/ColorPicker.tsx
index 2f372e3c0..8549424cd 100644
--- a/src/components/agents/ColorPicker.tsx
+++ b/src/components/agents/ColorPicker.tsx
@@ -1,111 +1,106 @@
-import { c as _c } from "react/compiler-runtime";
-import figures from 'figures';
-import React, { useState } from 'react';
-import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
-import { Box, Text } from '../../ink.js';
-import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from '../../tools/AgentTool/agentColorManager.js';
-import { capitalize } from '../../utils/stringUtils.js';
-type ColorOption = AgentColorName | 'automatic';
-const COLOR_OPTIONS: ColorOption[] = ['automatic', ...AGENT_COLORS];
+import figures from 'figures'
+import React, { useState } from 'react'
+import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
+import { Box, Text } from '../../ink.js'
+import {
+ AGENT_COLOR_TO_THEME_COLOR,
+ AGENT_COLORS,
+ type AgentColorName,
+} from '../../tools/AgentTool/agentColorManager.js'
+import { capitalize } from '../../utils/stringUtils.js'
+
+type ColorOption = AgentColorName | 'automatic'
+
+const COLOR_OPTIONS: ColorOption[] = ['automatic', ...AGENT_COLORS]
+
type Props = {
- agentName: string;
- currentColor?: AgentColorName | 'automatic';
- onConfirm: (color: AgentColorName | undefined) => void;
-};
-export function ColorPicker(t0) {
- const $ = _c(17);
- const {
- agentName,
- currentColor: t1,
- onConfirm
- } = t0;
- const currentColor = t1 === undefined ? "automatic" : t1;
- let t2;
- if ($[0] !== currentColor) {
- t2 = COLOR_OPTIONS.findIndex(opt => opt === currentColor);
- $[0] = currentColor;
- $[1] = t2;
- } else {
- t2 = $[1];
- }
- const [selectedIndex, setSelectedIndex] = useState(Math.max(0, t2));
- let t3;
- if ($[2] !== onConfirm || $[3] !== selectedIndex) {
- t3 = e => {
- if (e.key === "up") {
- e.preventDefault();
- setSelectedIndex(_temp);
- } else {
- if (e.key === "down") {
- e.preventDefault();
- setSelectedIndex(_temp2);
- } else {
- if (e.key === "return") {
- e.preventDefault();
- const selected = COLOR_OPTIONS[selectedIndex];
- onConfirm(selected === "automatic" ? undefined : selected);
- }
- }
- }
- };
- $[2] = onConfirm;
- $[3] = selectedIndex;
- $[4] = t3;
- } else {
- t3 = $[4];
- }
- const handleKeyDown = t3;
- const selectedValue = COLOR_OPTIONS[selectedIndex];
- let t4;
- if ($[5] !== selectedIndex) {
- t4 = COLOR_OPTIONS.map((option, index) => {
- const isSelected = index === selectedIndex;
- return {isSelected ? figures.pointer : " "}{option === "automatic" ? Automatic color : {" "}{capitalize(option)}};
- });
- $[5] = selectedIndex;
- $[6] = t4;
- } else {
- t4 = $[6];
- }
- let t5;
- if ($[7] !== t4) {
- t5 = {t4};
- $[7] = t4;
- $[8] = t5;
- } else {
- t5 = $[8];
- }
- let t6;
- if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
- t6 = Preview: ;
- $[9] = t6;
- } else {
- t6 = $[9];
- }
- let t7;
- if ($[10] !== agentName || $[11] !== selectedValue) {
- t7 = {t6}{selectedValue === undefined || selectedValue === "automatic" ? {" "}@{agentName}{" "} : {" "}@{agentName}{" "}};
- $[10] = agentName;
- $[11] = selectedValue;
- $[12] = t7;
- } else {
- t7 = $[12];
- }
- let t8;
- if ($[13] !== handleKeyDown || $[14] !== t5 || $[15] !== t7) {
- t8 = {t5}{t7};
- $[13] = handleKeyDown;
- $[14] = t5;
- $[15] = t7;
- $[16] = t8;
- } else {
- t8 = $[16];
- }
- return t8;
+ agentName: string
+ currentColor?: AgentColorName | 'automatic'
+ onConfirm: (color: AgentColorName | undefined) => void
}
-function _temp2(prev_0) {
- return prev_0 < COLOR_OPTIONS.length - 1 ? prev_0 + 1 : 0;
-}
-function _temp(prev) {
- return prev > 0 ? prev - 1 : COLOR_OPTIONS.length - 1;
+
+export function ColorPicker({
+ agentName,
+ currentColor = 'automatic',
+ onConfirm,
+}: Props): React.ReactNode {
+ const [selectedIndex, setSelectedIndex] = useState(
+ Math.max(
+ 0,
+ COLOR_OPTIONS.findIndex(opt => opt === currentColor),
+ ),
+ )
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'up') {
+ e.preventDefault()
+ setSelectedIndex(prev => (prev > 0 ? prev - 1 : COLOR_OPTIONS.length - 1))
+ } else if (e.key === 'down') {
+ e.preventDefault()
+ setSelectedIndex(prev => (prev < COLOR_OPTIONS.length - 1 ? prev + 1 : 0))
+ } else if (e.key === 'return') {
+ e.preventDefault()
+ const selected = COLOR_OPTIONS[selectedIndex]
+ onConfirm(selected === 'automatic' ? undefined : selected)
+ }
+ }
+
+ const selectedValue = COLOR_OPTIONS[selectedIndex]
+
+ return (
+
+
+ {COLOR_OPTIONS.map((option, index) => {
+ const isSelected = index === selectedIndex
+
+ return (
+
+
+ {isSelected ? figures.pointer : ' '}
+
+
+ {option === 'automatic' ? (
+ Automatic color
+ ) : (
+
+
+ {' '}
+
+ {capitalize(option)}
+
+ )}
+
+ )
+ })}
+
+
+
+ Preview:
+ {selectedValue === undefined || selectedValue === 'automatic' ? (
+
+ {' '}
+ @{agentName}{' '}
+
+ ) : (
+
+ {' '}
+ @{agentName}{' '}
+
+ )}
+
+
+ )
}
diff --git a/src/components/agents/ModelSelector.tsx b/src/components/agents/ModelSelector.tsx
index 9e186c7d2..4f1b2e8af 100644
--- a/src/components/agents/ModelSelector.tsx
+++ b/src/components/agents/ModelSelector.tsx
@@ -1,67 +1,52 @@
-import { c as _c } from "react/compiler-runtime";
-import * as React from 'react';
-import { Box, Text } from '../../ink.js';
-import { getAgentModelOptions } from '../../utils/model/agent.js';
-import { Select } from '../CustomSelect/select.js';
+import * as React from 'react'
+import { Box, Text } from '../../ink.js'
+import { getAgentModelOptions } from '../../utils/model/agent.js'
+import { Select } from '../CustomSelect/select.js'
+
interface ModelSelectorProps {
- initialModel?: string;
- onComplete: (model?: string) => void;
- onCancel?: () => void;
+ initialModel?: string
+ onComplete: (model?: string) => void
+ onCancel?: () => void
}
-export function ModelSelector(t0) {
- const $ = _c(11);
- const {
- initialModel,
- onComplete,
- onCancel
- } = t0;
- let t1;
- if ($[0] !== initialModel) {
- bb0: {
- const base = getAgentModelOptions();
- if (initialModel && !base.some(o => o.value === initialModel)) {
- t1 = [{
+
+export function ModelSelector({
+ initialModel,
+ onComplete,
+ onCancel,
+}: ModelSelectorProps): React.ReactNode {
+ const modelOptions = React.useMemo(() => {
+ const base = getAgentModelOptions()
+ // If the agent's current model is a full ID (e.g. 'claude-opus-4-5') not
+ // in the alias list, inject it as an option so it can round-trip through
+ // confirm without being overwritten.
+ if (initialModel && !base.some(o => o.value === initialModel)) {
+ return [
+ {
value: initialModel,
label: initialModel,
- description: "Current model (custom ID)"
- }, ...base];
- break bb0;
- }
- t1 = base;
+ description: 'Current model (custom ID)',
+ },
+ ...base,
+ ]
}
- $[0] = initialModel;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- const modelOptions = t1;
- const defaultModel = initialModel ?? "sonnet";
- let t2;
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
- t2 = Model determines the agent's reasoning capabilities and speed.;
- $[2] = t2;
- } else {
- t2 = $[2];
- }
- let t3;
- if ($[3] !== onCancel || $[4] !== onComplete) {
- t3 = () => onCancel ? onCancel() : onComplete(undefined);
- $[3] = onCancel;
- $[4] = onComplete;
- $[5] = t3;
- } else {
- t3 = $[5];
- }
- let t4;
- if ($[6] !== defaultModel || $[7] !== modelOptions || $[8] !== onComplete || $[9] !== t3) {
- t4 = {t2};
- $[6] = defaultModel;
- $[7] = modelOptions;
- $[8] = onComplete;
- $[9] = t3;
- $[10] = t4;
- } else {
- t4 = $[10];
- }
- return t4;
+ return base
+ }, [initialModel])
+
+ const defaultModel = initialModel ?? 'sonnet'
+
+ return (
+
+
+
+ Model determines the agent's reasoning capabilities and speed.
+
+
+
+ )
}
diff --git a/src/components/agents/ToolSelector.tsx b/src/components/agents/ToolSelector.tsx
index 27766abae..9bc20b7d8 100644
--- a/src/components/agents/ToolSelector.tsx
+++ b/src/components/agents/ToolSelector.tsx
@@ -1,561 +1,478 @@
-import { c as _c } from "react/compiler-runtime";
-import figures from 'figures';
-import React, { useCallback, useMemo, useState } from 'react';
-import { mcpInfoFromString } from 'src/services/mcp/mcpStringUtils.js';
-import { isMcpTool } from 'src/services/mcp/utils.js';
-import type { Tool, Tools } from 'src/Tool.js';
-import { filterToolsForAgent } from 'src/tools/AgentTool/agentToolUtils.js';
-import { AGENT_TOOL_NAME } from 'src/tools/AgentTool/constants.js';
-import { BashTool } from 'src/tools/BashTool/BashTool.js';
-import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js';
-import { FileEditTool } from 'src/tools/FileEditTool/FileEditTool.js';
-import { FileReadTool } from 'src/tools/FileReadTool/FileReadTool.js';
-import { FileWriteTool } from 'src/tools/FileWriteTool/FileWriteTool.js';
-import { GlobTool } from 'src/tools/GlobTool/GlobTool.js';
-import { GrepTool } from 'src/tools/GrepTool/GrepTool.js';
-import { ListMcpResourcesTool } from 'src/tools/ListMcpResourcesTool/ListMcpResourcesTool.js';
-import { NotebookEditTool } from 'src/tools/NotebookEditTool/NotebookEditTool.js';
-import { ReadMcpResourceTool } from 'src/tools/ReadMcpResourceTool/ReadMcpResourceTool.js';
-import { TaskOutputTool } from 'src/tools/TaskOutputTool/TaskOutputTool.js';
-import { TaskStopTool } from 'src/tools/TaskStopTool/TaskStopTool.js';
-import { TodoWriteTool } from 'src/tools/TodoWriteTool/TodoWriteTool.js';
-import { TungstenTool } from 'src/tools/TungstenTool/TungstenTool.js';
-import { WebFetchTool } from 'src/tools/WebFetchTool/WebFetchTool.js';
-import { WebSearchTool } from 'src/tools/WebSearchTool/WebSearchTool.js';
-import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
-import { Box, Text } from '../../ink.js';
-import { useKeybinding } from '../../keybindings/useKeybinding.js';
-import { count } from '../../utils/array.js';
-import { plural } from '../../utils/stringUtils.js';
-import { Divider } from '../design-system/Divider.js';
+import figures from 'figures'
+import React, { useCallback, useMemo, useState } from 'react'
+import { mcpInfoFromString } from 'src/services/mcp/mcpStringUtils.js'
+import { isMcpTool } from 'src/services/mcp/utils.js'
+import type { Tool, Tools } from 'src/Tool.js'
+import { filterToolsForAgent } from 'src/tools/AgentTool/agentToolUtils.js'
+import { AGENT_TOOL_NAME } from 'src/tools/AgentTool/constants.js'
+import { BashTool } from 'src/tools/BashTool/BashTool.js'
+import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
+import { FileEditTool } from 'src/tools/FileEditTool/FileEditTool.js'
+import { FileReadTool } from 'src/tools/FileReadTool/FileReadTool.js'
+import { FileWriteTool } from 'src/tools/FileWriteTool/FileWriteTool.js'
+import { GlobTool } from 'src/tools/GlobTool/GlobTool.js'
+import { GrepTool } from 'src/tools/GrepTool/GrepTool.js'
+import { ListMcpResourcesTool } from 'src/tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
+import { NotebookEditTool } from 'src/tools/NotebookEditTool/NotebookEditTool.js'
+import { ReadMcpResourceTool } from 'src/tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
+import { TaskOutputTool } from 'src/tools/TaskOutputTool/TaskOutputTool.js'
+import { TaskStopTool } from 'src/tools/TaskStopTool/TaskStopTool.js'
+import { TodoWriteTool } from 'src/tools/TodoWriteTool/TodoWriteTool.js'
+import { TungstenTool } from 'src/tools/TungstenTool/TungstenTool.js'
+import { WebFetchTool } from 'src/tools/WebFetchTool/WebFetchTool.js'
+import { WebSearchTool } from 'src/tools/WebSearchTool/WebSearchTool.js'
+import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
+import { Box, Text } from '../../ink.js'
+import { useKeybinding } from '../../keybindings/useKeybinding.js'
+import { count } from '../../utils/array.js'
+import { plural } from '../../utils/stringUtils.js'
+import { Divider } from '../design-system/Divider.js'
+
type Props = {
- tools: Tools;
- initialTools: string[] | undefined;
- onComplete: (selectedTools: string[] | undefined) => void;
- onCancel?: () => void;
-};
+ tools: Tools
+ initialTools: string[] | undefined
+ onComplete: (selectedTools: string[] | undefined) => void
+ onCancel?: () => void
+}
+
type ToolBucket = {
- name: string;
- toolNames: Set;
- isMcp?: boolean;
-};
+ name: string
+ toolNames: Set
+ isMcp?: boolean
+}
+
type ToolBuckets = {
- READ_ONLY: ToolBucket;
- EDIT: ToolBucket;
- EXECUTION: ToolBucket;
- MCP: ToolBucket;
- OTHER: ToolBucket;
-};
+ READ_ONLY: ToolBucket
+ EDIT: ToolBucket
+ EXECUTION: ToolBucket
+ MCP: ToolBucket
+ OTHER: ToolBucket
+}
+
function getToolBuckets(): ToolBuckets {
return {
READ_ONLY: {
name: 'Read-only tools',
- toolNames: new Set([GlobTool.name, GrepTool.name, ExitPlanModeV2Tool.name, FileReadTool.name, WebFetchTool.name, TodoWriteTool.name, WebSearchTool.name, TaskStopTool.name, TaskOutputTool.name, ListMcpResourcesTool.name, ReadMcpResourceTool.name])
+ toolNames: new Set([
+ GlobTool.name,
+ GrepTool.name,
+ ExitPlanModeV2Tool.name,
+ FileReadTool.name,
+ WebFetchTool.name,
+ TodoWriteTool.name,
+ WebSearchTool.name,
+ TaskStopTool.name,
+ TaskOutputTool.name,
+ ListMcpResourcesTool.name,
+ ReadMcpResourceTool.name,
+ ]),
},
EDIT: {
name: 'Edit tools',
- toolNames: new Set([FileEditTool.name, FileWriteTool.name, NotebookEditTool.name])
+ toolNames: new Set([
+ FileEditTool.name,
+ FileWriteTool.name,
+ NotebookEditTool.name,
+ ]),
},
EXECUTION: {
name: 'Execution tools',
- toolNames: new Set([BashTool.name, (process.env.USER_TYPE) === 'ant' ? TungstenTool.name : undefined].filter(n => n !== undefined))
+ toolNames: new Set(
+ [
+ BashTool.name,
+ process.env.USER_TYPE === 'ant' ? TungstenTool.name : undefined,
+ ].filter(n => n !== undefined),
+ ),
},
MCP: {
name: 'MCP tools',
- toolNames: new Set(),
- // Dynamic - no static list
- isMcp: true
+ toolNames: new Set(), // Dynamic - no static list
+ isMcp: true,
},
OTHER: {
name: 'Other tools',
- toolNames: new Set() // Dynamic - catch-all for uncategorized tools
- }
- };
+ toolNames: new Set(), // Dynamic - catch-all for uncategorized tools
+ },
+ }
}
// Helper to get MCP server buckets dynamically
function getMcpServerBuckets(tools: Tools): Array<{
- serverName: string;
- tools: Tools;
+ serverName: string
+ tools: Tools
}> {
- const serverMap = new Map();
+ const serverMap = new Map()
+
tools.forEach(tool => {
if (isMcpTool(tool)) {
- const mcpInfo = mcpInfoFromString(tool.name);
+ const mcpInfo = mcpInfoFromString(tool.name)
if (mcpInfo?.serverName) {
- const existing = serverMap.get(mcpInfo.serverName) || [];
- existing.push(tool);
- serverMap.set(mcpInfo.serverName, existing);
+ const existing = serverMap.get(mcpInfo.serverName) || []
+ existing.push(tool)
+ serverMap.set(mcpInfo.serverName, existing)
}
}
- });
- return Array.from(serverMap.entries()).map(([serverName, tools]) => ({
- serverName,
- tools
- })).sort((a, b) => a.serverName.localeCompare(b.serverName));
+ })
+
+ return Array.from(serverMap.entries())
+ .map(([serverName, tools]) => ({ serverName, tools }))
+ .sort((a, b) => a.serverName.localeCompare(b.serverName))
}
-export function ToolSelector(t0) {
- const $ = _c(69);
- const {
- tools,
- initialTools,
- onComplete,
- onCancel
- } = t0;
- let t1;
- if ($[0] !== tools) {
- t1 = filterToolsForAgent({
- tools,
- isBuiltIn: false,
- isAsync: false
- });
- $[0] = tools;
- $[1] = t1;
- } else {
- t1 = $[1];
+
+export function ToolSelector({
+ tools,
+ initialTools,
+ onComplete,
+ onCancel,
+}: Props): React.ReactNode {
+ // Filter tools for custom agents
+ const customAgentTools = useMemo(
+ () => filterToolsForAgent({ tools, isBuiltIn: false, isAsync: false }),
+ [tools],
+ )
+
+ // Expand wildcard or undefined to explicit tool list for internal state
+ const expandedInitialTools =
+ !initialTools || initialTools.includes('*')
+ ? customAgentTools.map(t => t.name)
+ : initialTools
+
+ const [selectedTools, setSelectedTools] =
+ useState(expandedInitialTools)
+ const [focusIndex, setFocusIndex] = useState(0)
+ const [showIndividualTools, setShowIndividualTools] = useState(false)
+
+ // Filter selectedTools to only include tools that currently exist
+ // This handles MCP tools that disconnect while selected
+ const validSelectedTools = useMemo(() => {
+ const toolNames = new Set(customAgentTools.map(t => t.name))
+ return selectedTools.filter(name => toolNames.has(name))
+ }, [selectedTools, customAgentTools])
+
+ const selectedSet = new Set(validSelectedTools)
+ const isAllSelected =
+ validSelectedTools.length === customAgentTools.length &&
+ customAgentTools.length > 0
+
+ const handleToggleTool = (toolName: string) => {
+ if (!toolName) return
+
+ setSelectedTools(current =>
+ current.includes(toolName)
+ ? current.filter(t => t !== toolName)
+ : [...current, toolName],
+ )
}
- const customAgentTools = t1;
- let t2;
- if ($[2] !== customAgentTools || $[3] !== initialTools) {
- t2 = !initialTools || initialTools.includes("*") ? customAgentTools.map(_temp) : initialTools;
- $[2] = customAgentTools;
- $[3] = initialTools;
- $[4] = t2;
- } else {
- t2 = $[4];
- }
- const expandedInitialTools = t2;
- const [selectedTools, setSelectedTools] = useState(expandedInitialTools);
- const [focusIndex, setFocusIndex] = useState(0);
- const [showIndividualTools, setShowIndividualTools] = useState(false);
- let t3;
- if ($[5] !== customAgentTools) {
- t3 = new Set(customAgentTools.map(_temp2));
- $[5] = customAgentTools;
- $[6] = t3;
- } else {
- t3 = $[6];
- }
- const toolNames = t3;
- let t4;
- if ($[7] !== selectedTools || $[8] !== toolNames) {
- let t5;
- if ($[10] !== toolNames) {
- t5 = name => toolNames.has(name);
- $[10] = toolNames;
- $[11] = t5;
- } else {
- t5 = $[11];
- }
- t4 = selectedTools.filter(t5);
- $[7] = selectedTools;
- $[8] = toolNames;
- $[9] = t4;
- } else {
- t4 = $[9];
- }
- const validSelectedTools = t4;
- let t5;
- if ($[12] !== validSelectedTools) {
- t5 = new Set(validSelectedTools);
- $[12] = validSelectedTools;
- $[13] = t5;
- } else {
- t5 = $[13];
- }
- const selectedSet = t5;
- const isAllSelected = validSelectedTools.length === customAgentTools.length && customAgentTools.length > 0;
- let t6;
- if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
- t6 = toolName => {
- if (!toolName) {
- return;
+
+ const handleToggleTools = (toolNames: string[], select: boolean) => {
+ setSelectedTools(current => {
+ if (select) {
+ const toolsToAdd = toolNames.filter(t => !current.includes(t))
+ return [...current, ...toolsToAdd]
+ } else {
+ return current.filter(t => !toolNames.includes(t))
}
- setSelectedTools(current => current.includes(toolName) ? current.filter(t_1 => t_1 !== toolName) : [...current, toolName]);
- };
- $[14] = t6;
- } else {
- t6 = $[14];
+ })
}
- const handleToggleTool = t6;
- let t7;
- if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
- t7 = (toolNames_0, select) => {
- setSelectedTools(current_0 => {
- if (select) {
- const toolsToAdd = toolNames_0.filter(t_2 => !current_0.includes(t_2));
- return [...current_0, ...toolsToAdd];
- } else {
- return current_0.filter(t_3 => !toolNames_0.includes(t_3));
- }
- });
- };
- $[15] = t7;
- } else {
- t7 = $[15];
+
+ const handleConfirm = () => {
+ // Convert to undefined if all tools are selected (for cleaner file format)
+ const allToolNames = customAgentTools.map(t => t.name)
+ const areAllToolsSelected =
+ validSelectedTools.length === allToolNames.length &&
+ allToolNames.every(name => validSelectedTools.includes(name))
+ const finalTools = areAllToolsSelected ? undefined : validSelectedTools
+
+ onComplete(finalTools)
}
- const handleToggleTools = t7;
- let t8;
- if ($[16] !== customAgentTools || $[17] !== onComplete || $[18] !== validSelectedTools) {
- t8 = () => {
- const allToolNames = customAgentTools.map(_temp3);
- const areAllToolsSelected = validSelectedTools.length === allToolNames.length && allToolNames.every(name_0 => validSelectedTools.includes(name_0));
- const finalTools = areAllToolsSelected ? undefined : validSelectedTools;
- onComplete(finalTools);
- };
- $[16] = customAgentTools;
- $[17] = onComplete;
- $[18] = validSelectedTools;
- $[19] = t8;
- } else {
- t8 = $[19];
- }
- const handleConfirm = t8;
- let buckets;
- if ($[20] !== customAgentTools) {
- const toolBuckets = getToolBuckets();
- buckets = {
+
+ // Group tools by bucket
+ const toolsByBucket = useMemo(() => {
+ const toolBuckets = getToolBuckets()
+ const buckets = {
readOnly: [] as Tool[],
edit: [] as Tool[],
execution: [] as Tool[],
mcp: [] as Tool[],
- other: [] as Tool[]
- };
+ other: [] as Tool[],
+ }
+
customAgentTools.forEach(tool => {
+ // Check if it's an MCP tool first
if (isMcpTool(tool)) {
- buckets.mcp.push(tool);
- } else {
- if (toolBuckets.READ_ONLY.toolNames.has(tool.name)) {
- buckets.readOnly.push(tool);
- } else {
- if (toolBuckets.EDIT.toolNames.has(tool.name)) {
- buckets.edit.push(tool);
- } else {
- if (toolBuckets.EXECUTION.toolNames.has(tool.name)) {
- buckets.execution.push(tool);
- } else {
- if (tool.name !== AGENT_TOOL_NAME) {
- buckets.other.push(tool);
- }
- }
- }
- }
+ buckets.mcp.push(tool)
+ } else if (toolBuckets.READ_ONLY.toolNames.has(tool.name)) {
+ buckets.readOnly.push(tool)
+ } else if (toolBuckets.EDIT.toolNames.has(tool.name)) {
+ buckets.edit.push(tool)
+ } else if (toolBuckets.EXECUTION.toolNames.has(tool.name)) {
+ buckets.execution.push(tool)
+ } else if (tool.name !== AGENT_TOOL_NAME) {
+ // Catch-all for uncategorized tools (except Task)
+ buckets.other.push(tool)
}
- });
- $[20] = customAgentTools;
- $[21] = buckets;
- } else {
- buckets = $[21];
- }
- const toolsByBucket = buckets;
- let t9;
- if ($[22] !== selectedSet) {
- t9 = bucketTools => {
- const selected = count(bucketTools, (t_5: Tool) => selectedSet.has(t_5.name));
- const needsSelection = selected < bucketTools.length;
- return () => {
- const toolNames_1 = bucketTools.map(_temp4);
- handleToggleTools(toolNames_1, needsSelection);
- };
- };
- $[22] = selectedSet;
- $[23] = t9;
- } else {
- t9 = $[23];
- }
- const createBucketToggleAction = t9;
- let navigableItems;
- if ($[24] !== createBucketToggleAction || $[25] !== customAgentTools || $[26] !== focusIndex || $[27] !== handleConfirm || $[28] !== isAllSelected || $[29] !== selectedSet || $[30] !== showIndividualTools || $[31] !== toolsByBucket.edit || $[32] !== toolsByBucket.execution || $[33] !== toolsByBucket.mcp || $[34] !== toolsByBucket.other || $[35] !== toolsByBucket.readOnly) {
- navigableItems = [];
- navigableItems.push({
- id: "continue",
- label: "Continue",
- action: handleConfirm,
- isContinue: true
- });
- let t10;
- if ($[37] !== customAgentTools || $[38] !== isAllSelected) {
- t10 = () => {
- const allToolNames_0 = customAgentTools.map(_temp5);
- handleToggleTools(allToolNames_0, !isAllSelected);
- };
- $[37] = customAgentTools;
- $[38] = isAllSelected;
- $[39] = t10;
- } else {
- t10 = $[39];
+ })
+
+ return buckets
+ }, [customAgentTools])
+
+ const createBucketToggleAction = (bucketTools: Tool[]) => {
+ const selected = count(bucketTools, t => selectedSet.has(t.name))
+ const needsSelection = selected < bucketTools.length
+
+ return () => {
+ const toolNames = bucketTools.map(t => t.name)
+ handleToggleTools(toolNames, needsSelection)
}
+ }
+
+ // Build navigable items (no separators)
+ const navigableItems: Array<{
+ id: string
+ label: string
+ action: () => void
+ isContinue?: boolean
+ isToggle?: boolean
+ isHeader?: boolean
+ }> = []
+
+ // Continue button
+ navigableItems.push({
+ id: 'continue',
+ label: 'Continue',
+ action: handleConfirm,
+ isContinue: true,
+ })
+
+ // All tools
+ navigableItems.push({
+ id: 'bucket-all',
+ label: `${isAllSelected ? figures.checkboxOn : figures.checkboxOff} All tools`,
+ action: () => {
+ const allToolNames = customAgentTools.map(t => t.name)
+ handleToggleTools(allToolNames, !isAllSelected)
+ },
+ })
+
+ // Create bucket menu items
+ const toolBuckets = getToolBuckets()
+ const bucketConfigs = [
+ {
+ id: 'bucket-readonly',
+ name: toolBuckets.READ_ONLY.name,
+ tools: toolsByBucket.readOnly,
+ },
+ {
+ id: 'bucket-edit',
+ name: toolBuckets.EDIT.name,
+ tools: toolsByBucket.edit,
+ },
+ {
+ id: 'bucket-execution',
+ name: toolBuckets.EXECUTION.name,
+ tools: toolsByBucket.execution,
+ },
+ {
+ id: 'bucket-mcp',
+ name: toolBuckets.MCP.name,
+ tools: toolsByBucket.mcp,
+ },
+ {
+ id: 'bucket-other',
+ name: toolBuckets.OTHER.name,
+ tools: toolsByBucket.other,
+ },
+ ]
+
+ bucketConfigs.forEach(({ id, name, tools: bucketTools }) => {
+ if (bucketTools.length === 0) return
+
+ const selected = count(bucketTools, t => selectedSet.has(t.name))
+ const isFullySelected = selected === bucketTools.length
+
navigableItems.push({
- id: "bucket-all",
- label: `${isAllSelected ? figures.checkboxOn : figures.checkboxOff} All tools`,
- action: t10
- });
- const toolBuckets_0 = getToolBuckets();
- const bucketConfigs = [{
- id: "bucket-readonly",
- name: toolBuckets_0.READ_ONLY.name,
- tools: toolsByBucket.readOnly
- }, {
- id: "bucket-edit",
- name: toolBuckets_0.EDIT.name,
- tools: toolsByBucket.edit
- }, {
- id: "bucket-execution",
- name: toolBuckets_0.EXECUTION.name,
- tools: toolsByBucket.execution
- }, {
- id: "bucket-mcp",
- name: toolBuckets_0.MCP.name,
- tools: toolsByBucket.mcp
- }, {
- id: "bucket-other",
- name: toolBuckets_0.OTHER.name,
- tools: toolsByBucket.other
- }];
- bucketConfigs.forEach(t11 => {
- const {
- id,
- name: name_1,
- tools: bucketTools_0
- } = t11;
- if (bucketTools_0.length === 0) {
- return;
+ id,
+ label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${name}`,
+ action: createBucketToggleAction(bucketTools),
+ })
+ })
+
+ // Toggle button for individual tools
+ const toggleButtonIndex = navigableItems.length
+ navigableItems.push({
+ id: 'toggle-individual',
+ label: showIndividualTools
+ ? 'Hide advanced options'
+ : 'Show advanced options',
+ action: () => {
+ setShowIndividualTools(!showIndividualTools)
+ // If hiding tools and focus is on an individual tool, move focus to toggle button
+ if (showIndividualTools && focusIndex > toggleButtonIndex) {
+ setFocusIndex(toggleButtonIndex)
}
- const selected_0 = count(bucketTools_0, (t_8: Tool) => selectedSet.has(t_8.name));
- const isFullySelected = selected_0 === bucketTools_0.length;
+ },
+ isToggle: true,
+ })
+
+ // Memoize MCP server buckets (must be outside conditional for hooks rules)
+ const mcpServerBuckets = useMemo(
+ () => getMcpServerBuckets(customAgentTools),
+ [customAgentTools],
+ )
+
+ // Individual tools (only if expanded)
+ if (showIndividualTools) {
+ // Add MCP server buckets if any exist
+ if (mcpServerBuckets.length > 0) {
navigableItems.push({
- id,
- label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${name_1}`,
- action: createBucketToggleAction(bucketTools_0)
- });
- });
- const toggleButtonIndex = navigableItems.length;
- let t12;
- if ($[40] !== focusIndex || $[41] !== showIndividualTools || $[42] !== toggleButtonIndex) {
- t12 = () => {
- setShowIndividualTools(!showIndividualTools);
- if (showIndividualTools && focusIndex > toggleButtonIndex) {
- setFocusIndex(toggleButtonIndex);
- }
- };
- $[40] = focusIndex;
- $[41] = showIndividualTools;
- $[42] = toggleButtonIndex;
- $[43] = t12;
+ id: 'mcp-servers-header',
+ label: 'MCP Servers:',
+ action: () => {}, // No action - just a header
+ isHeader: true,
+ })
+
+ mcpServerBuckets.forEach(({ serverName, tools: serverTools }) => {
+ const selected = count(serverTools, t => selectedSet.has(t.name))
+ const isFullySelected = selected === serverTools.length
+
+ navigableItems.push({
+ id: `mcp-server-${serverName}`,
+ label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${serverName} (${serverTools.length} ${plural(serverTools.length, 'tool')})`,
+ action: () => {
+ const toolNames = serverTools.map(t => t.name)
+ handleToggleTools(toolNames, !isFullySelected)
+ },
+ })
+ })
+
+ // Add separator header before individual tools
+ navigableItems.push({
+ id: 'tools-header',
+ label: 'Individual Tools:',
+ action: () => {},
+ isHeader: true,
+ })
+ }
+
+ // Add individual tools
+ customAgentTools.forEach(tool => {
+ let displayName = tool.name
+ if (tool.name.startsWith('mcp__')) {
+ const mcpInfo = mcpInfoFromString(tool.name)
+ displayName = mcpInfo
+ ? `${mcpInfo.toolName} (${mcpInfo.serverName})`
+ : tool.name
+ }
+
+ navigableItems.push({
+ id: `tool-${tool.name}`,
+ label: `${selectedSet.has(tool.name) ? figures.checkboxOn : figures.checkboxOff} ${displayName}`,
+ action: () => handleToggleTool(tool.name),
+ })
+ })
+ }
+
+ const handleCancel = useCallback(() => {
+ if (onCancel) {
+ onCancel()
} else {
- t12 = $[43];
+ onComplete(initialTools)
}
- navigableItems.push({
- id: "toggle-individual",
- label: showIndividualTools ? "Hide advanced options" : "Show advanced options",
- action: t12,
- isToggle: true
- });
- const mcpServerBuckets = getMcpServerBuckets(customAgentTools);
- if (showIndividualTools) {
- if (mcpServerBuckets.length > 0) {
- navigableItems.push({
- id: "mcp-servers-header",
- label: "MCP Servers:",
- action: _temp6,
- isHeader: true
- });
- mcpServerBuckets.forEach(t13 => {
- const {
- serverName,
- tools: serverTools
- } = t13;
- const selected_1 = count(serverTools, t_9 => selectedSet.has(t_9.name));
- const isFullySelected_0 = selected_1 === serverTools.length;
- navigableItems.push({
- id: `mcp-server-${serverName}`,
- label: `${isFullySelected_0 ? figures.checkboxOn : figures.checkboxOff} ${serverName} (${serverTools.length} ${plural(serverTools.length, "tool")})`,
- action: () => {
- const toolNames_2 = serverTools.map(_temp7);
- handleToggleTools(toolNames_2, !isFullySelected_0);
- }
- });
- });
- navigableItems.push({
- id: "tools-header",
- label: "Individual Tools:",
- action: _temp8,
- isHeader: true
- });
+ }, [onCancel, onComplete, initialTools])
+
+ useKeybinding('confirm:no', handleCancel, { context: 'Confirmation' })
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'return') {
+ e.preventDefault()
+ const item = navigableItems[focusIndex]
+ if (item && !item.isHeader) {
+ item.action()
}
- customAgentTools.forEach(tool_0 => {
- let displayName = tool_0.name;
- if (tool_0.name.startsWith("mcp__")) {
- const mcpInfo = mcpInfoFromString(tool_0.name);
- displayName = mcpInfo ? `${mcpInfo.toolName} (${mcpInfo.serverName})` : tool_0.name;
- }
- navigableItems.push({
- id: `tool-${tool_0.name}`,
- label: `${selectedSet.has(tool_0.name) ? figures.checkboxOn : figures.checkboxOff} ${displayName}`,
- action: () => handleToggleTool(tool_0.name)
- });
- });
+ } else if (e.key === 'up') {
+ e.preventDefault()
+ let newIndex = focusIndex - 1
+ // Skip headers when navigating up
+ while (newIndex > 0 && navigableItems[newIndex]?.isHeader) {
+ newIndex--
+ }
+ setFocusIndex(Math.max(0, newIndex))
+ } else if (e.key === 'down') {
+ e.preventDefault()
+ let newIndex = focusIndex + 1
+ // Skip headers when navigating down
+ while (
+ newIndex < navigableItems.length - 1 &&
+ navigableItems[newIndex]?.isHeader
+ ) {
+ newIndex++
+ }
+ setFocusIndex(Math.min(navigableItems.length - 1, newIndex))
}
- $[24] = createBucketToggleAction;
- $[25] = customAgentTools;
- $[26] = focusIndex;
- $[27] = handleConfirm;
- $[28] = isAllSelected;
- $[29] = selectedSet;
- $[30] = showIndividualTools;
- $[31] = toolsByBucket.edit;
- $[32] = toolsByBucket.execution;
- $[33] = toolsByBucket.mcp;
- $[34] = toolsByBucket.other;
- $[35] = toolsByBucket.readOnly;
- $[36] = navigableItems;
- } else {
- navigableItems = $[36];
}
- let t10;
- if ($[44] !== initialTools || $[45] !== onCancel || $[46] !== onComplete) {
- t10 = () => {
- if (onCancel) {
- onCancel();
- } else {
- onComplete(initialTools);
- }
- };
- $[44] = initialTools;
- $[45] = onCancel;
- $[46] = onComplete;
- $[47] = t10;
- } else {
- t10 = $[47];
- }
- const handleCancel = t10;
- let t11;
- if ($[48] === Symbol.for("react.memo_cache_sentinel")) {
- t11 = {
- context: "Confirmation"
- };
- $[48] = t11;
- } else {
- t11 = $[48];
- }
- useKeybinding("confirm:no", handleCancel, t11);
- let t12;
- if ($[49] !== focusIndex || $[50] !== navigableItems) {
- t12 = e => {
- if (e.key === "return") {
- e.preventDefault();
- const item = navigableItems[focusIndex];
- if (item && !item.isHeader) {
- item.action();
- }
- } else {
- if (e.key === "up") {
- e.preventDefault();
- let newIndex = focusIndex - 1;
- while (newIndex > 0 && navigableItems[newIndex]?.isHeader) {
- newIndex--;
- }
- setFocusIndex(Math.max(0, newIndex));
- } else {
- if (e.key === "down") {
- e.preventDefault();
- let newIndex_0 = focusIndex + 1;
- while (newIndex_0 < navigableItems.length - 1 && navigableItems[newIndex_0]?.isHeader) {
- newIndex_0++;
- }
- setFocusIndex(Math.min(navigableItems.length - 1, newIndex_0));
- }
- }
- }
- };
- $[49] = focusIndex;
- $[50] = navigableItems;
- $[51] = t12;
- } else {
- t12 = $[51];
- }
- const handleKeyDown = t12;
- const t13 = focusIndex === 0 ? "suggestion" : undefined;
- const t14 = focusIndex === 0;
- const t15 = focusIndex === 0 ? `${figures.pointer} ` : " ";
- let t16;
- if ($[52] !== t13 || $[53] !== t14 || $[54] !== t15) {
- t16 = {t15}[ Continue ];
- $[52] = t13;
- $[53] = t14;
- $[54] = t15;
- $[55] = t16;
- } else {
- t16 = $[55];
- }
- let t17;
- if ($[56] === Symbol.for("react.memo_cache_sentinel")) {
- t17 = ;
- $[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 {isToggleButton && }{isHeader && index > 0 && }{isHeader ? "" : isCurrentlyFocused ? `${figures.pointer} ` : " "}{isToggleButton ? `[ ${item_0.label} ]` : item_0.label};
- });
- $[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 = {t20};
- $[62] = t20;
- $[63] = t21;
- } else {
- t21 = $[63];
- }
- let t22;
- if ($[64] !== handleKeyDown || $[65] !== t16 || $[66] !== t19 || $[67] !== t21) {
- t22 = {t16}{t17}{t19}{t21};
- $[64] = handleKeyDown;
- $[65] = t16;
- $[66] = t19;
- $[67] = t21;
- $[68] = t22;
- } else {
- t22 = $[68];
- }
- return t22;
-}
-function _temp8() {}
-function _temp7(t_10) {
- return t_10.name;
-}
-function _temp6() {}
-function _temp5(t_7) {
- return t_7.name;
-}
-function _temp4(t_6) {
- return t_6.name;
-}
-function _temp3(t_4) {
- return t_4.name;
-}
-function _temp2(t_0) {
- return t_0.name;
-}
-function _temp(t) {
- return t.name;
+
+ return (
+
+ {/* Render Continue button */}
+
+ {focusIndex === 0 ? `${figures.pointer} ` : ' '}[ Continue ]
+
+
+ {/* Separator */}
+
+
+ {/* Render all navigable items except Continue (which is at index 0) */}
+ {navigableItems.slice(1).map((item, index) => {
+ const isCurrentlyFocused = index + 1 === focusIndex
+ const isToggleButton = item.isToggle
+ const isHeader = item.isHeader
+
+ return (
+
+ {/* Add separator before toggle button */}
+ {isToggleButton && }
+
+ {/* Add margin before headers */}
+ {isHeader && index > 0 && }
+
+
+ {isHeader
+ ? ''
+ : isCurrentlyFocused
+ ? `${figures.pointer} `
+ : ' '}
+ {isToggleButton ? `[ ${item.label} ]` : item.label}
+
+
+ )
+ })}
+
+
+
+ {isAllSelected
+ ? 'All tools selected'
+ : `${selectedSet.size} of ${customAgentTools.length} tools selected`}
+
+
+
+ )
}
diff --git a/src/components/agents/new-agent-creation/CreateAgentWizard.tsx b/src/components/agents/new-agent-creation/CreateAgentWizard.tsx
index bad4005a4..b9959d91d 100644
--- a/src/components/agents/new-agent-creation/CreateAgentWizard.tsx
+++ b/src/components/agents/new-agent-creation/CreateAgentWizard.tsx
@@ -1,96 +1,68 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { type ReactNode } from 'react';
-import { isAutoMemoryEnabled } from '../../../memdir/paths.js';
-import type { Tools } from '../../../Tool.js';
-import type { AgentDefinition } from '../../../tools/AgentTool/loadAgentsDir.js';
-import { WizardProvider } from '../../wizard/index.js';
-import type { WizardStepComponent } from '../../wizard/types.js';
-import type { AgentWizardData } from './types.js';
-import { ColorStep } from './wizard-steps/ColorStep.js';
-import { ConfirmStepWrapper } from './wizard-steps/ConfirmStepWrapper.js';
-import { DescriptionStep } from './wizard-steps/DescriptionStep.js';
-import { GenerateStep } from './wizard-steps/GenerateStep.js';
-import { LocationStep } from './wizard-steps/LocationStep.js';
-import { MemoryStep } from './wizard-steps/MemoryStep.js';
-import { MethodStep } from './wizard-steps/MethodStep.js';
-import { ModelStep } from './wizard-steps/ModelStep.js';
-import { PromptStep } from './wizard-steps/PromptStep.js';
-import { ToolsStep } from './wizard-steps/ToolsStep.js';
-import { TypeStep } from './wizard-steps/TypeStep.js';
+import React, { type ReactNode } from 'react'
+import { isAutoMemoryEnabled } from '../../../memdir/paths.js'
+import type { Tools } from '../../../Tool.js'
+import type { AgentDefinition } from '../../../tools/AgentTool/loadAgentsDir.js'
+import { WizardProvider } from '../../wizard/index.js'
+import type { WizardStepComponent } from '../../wizard/types.js'
+import type { AgentWizardData } from './types.js'
+import { ColorStep } from './wizard-steps/ColorStep.js'
+import { ConfirmStepWrapper } from './wizard-steps/ConfirmStepWrapper.js'
+import { DescriptionStep } from './wizard-steps/DescriptionStep.js'
+import { GenerateStep } from './wizard-steps/GenerateStep.js'
+import { LocationStep } from './wizard-steps/LocationStep.js'
+import { MemoryStep } from './wizard-steps/MemoryStep.js'
+import { MethodStep } from './wizard-steps/MethodStep.js'
+import { ModelStep } from './wizard-steps/ModelStep.js'
+import { PromptStep } from './wizard-steps/PromptStep.js'
+import { ToolsStep } from './wizard-steps/ToolsStep.js'
+import { TypeStep } from './wizard-steps/TypeStep.js'
+
type Props = {
- tools: Tools;
- existingAgents: AgentDefinition[];
- onComplete: (message: string) => void;
- onCancel: () => void;
-};
-export function CreateAgentWizard(t0) {
- const $ = _c(17);
- const {
- tools,
- existingAgents,
- onComplete,
- onCancel
- } = t0;
- let t1;
- if ($[0] !== existingAgents) {
- t1 = () => ;
- $[0] = existingAgents;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- let t2;
- if ($[2] !== tools) {
- t2 = () => ;
- $[2] = tools;
- $[3] = t2;
- } else {
- t2 = $[3];
- }
- let t3;
- if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
- t3 = isAutoMemoryEnabled() ? [MemoryStep] : [];
- $[4] = t3;
- } else {
- t3 = $[4];
- }
- let t4;
- if ($[5] !== existingAgents || $[6] !== onComplete || $[7] !== tools) {
- t4 = () => ;
- $[5] = existingAgents;
- $[6] = onComplete;
- $[7] = tools;
- $[8] = t4;
- } else {
- t4 = $[8];
- }
- let t5;
- if ($[9] !== t1 || $[10] !== t2 || $[11] !== t4) {
- t5 = [LocationStep, MethodStep, GenerateStep, t1, PromptStep, DescriptionStep, t2, ModelStep, ColorStep, ...t3, t4];
- $[9] = t1;
- $[10] = t2;
- $[11] = t4;
- $[12] = t5;
- } else {
- t5 = $[12];
- }
- const steps = t5;
- let t6;
- if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
- t6 = {};
- $[13] = t6;
- } else {
- t6 = $[13];
- }
- let t7;
- if ($[14] !== onCancel || $[15] !== steps) {
- t7 = ;
- $[14] = onCancel;
- $[15] = steps;
- $[16] = t7;
- } else {
- t7 = $[16];
- }
- return t7;
+ tools: Tools
+ existingAgents: AgentDefinition[]
+ onComplete: (message: string) => void
+ onCancel: () => void
+}
+
+export function CreateAgentWizard({
+ tools,
+ existingAgents,
+ onComplete,
+ onCancel,
+}: Props): ReactNode {
+ // Create step components with props
+ const steps: WizardStepComponent[] = [
+ LocationStep, // 0
+ MethodStep, // 1
+ GenerateStep, // 2
+ () => , // 3
+ PromptStep, // 4
+ DescriptionStep, // 5
+ () => , // 6
+ ModelStep, // 7
+ ColorStep, // 8
+ // MemoryStep is conditionally included based on GrowthBook gate
+ ...(isAutoMemoryEnabled() ? [MemoryStep] : []),
+ () => (
+
+ ),
+ ]
+
+ return (
+
+ steps={steps}
+ initialData={{}}
+ onComplete={() => {
+ // Wizard completion is handled by ConfirmStepWrapper
+ // which calls onComplete with the appropriate message
+ }}
+ onCancel={onCancel}
+ title="Create new agent"
+ showStepCounter={false}
+ />
+ )
}
-function _temp() {}
diff --git a/src/components/agents/new-agent-creation/wizard-steps/ColorStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/ColorStep.tsx
index 9ec059371..adc35e27c 100644
--- a/src/components/agents/new-agent-creation/wizard-steps/ColorStep.tsx
+++ b/src/components/agents/new-agent-creation/wizard-steps/ColorStep.tsx
@@ -1,83 +1,64 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { type ReactNode } from 'react';
-import { Box } from '../../../../ink.js';
-import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
-import type { AgentColorName } from '../../../../tools/AgentTool/agentColorManager.js';
-import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
-import { Byline } from '../../../design-system/Byline.js';
-import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
-import { useWizard } from '../../../wizard/index.js';
-import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
-import { ColorPicker } from '../../ColorPicker.js';
-import type { AgentWizardData } from '../types.js';
-export function ColorStep() {
- const $ = _c(14);
- const {
- goNext,
- goBack,
- updateWizardData,
- wizardData
- } = useWizard();
- let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t0 = {
- context: "Confirmation"
- };
- $[0] = t0;
- } else {
- t0 = $[0];
+import React, { type ReactNode } from 'react'
+import { Box } from '../../../../ink.js'
+import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
+import type { AgentColorName } from '../../../../tools/AgentTool/agentColorManager.js'
+import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
+import { Byline } from '../../../design-system/Byline.js'
+import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
+import { useWizard } from '../../../wizard/index.js'
+import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
+import { ColorPicker } from '../../ColorPicker.js'
+import type { AgentWizardData } from '../types.js'
+
+export function ColorStep(): ReactNode {
+ const { goNext, goBack, updateWizardData, wizardData } =
+ useWizard()
+
+ // Handle escape key - ColorPicker handles its own escape internally
+ useKeybinding('confirm:no', goBack, { context: 'Confirmation' })
+
+ const handleConfirm = (color?: string): void => {
+ updateWizardData({
+ selectedColor: color,
+ // Prepare final agent for confirmation
+ finalAgent: {
+ agentType: wizardData.agentType!,
+ whenToUse: wizardData.whenToUse!,
+ getSystemPrompt: () => wizardData.systemPrompt!,
+ tools: wizardData.selectedTools,
+ ...(wizardData.selectedModel
+ ? { model: wizardData.selectedModel }
+ : {}),
+ ...(color ? { color: color as AgentColorName } : {}),
+ source: wizardData.location!,
+ },
+ })
+ goNext()
}
- useKeybinding("confirm:no", goBack, t0);
- let t1;
- if ($[1] !== goNext || $[2] !== updateWizardData || $[3] !== wizardData.agentType || $[4] !== wizardData.location || $[5] !== wizardData.selectedModel || $[6] !== wizardData.selectedTools || $[7] !== wizardData.systemPrompt || $[8] !== wizardData.whenToUse) {
- t1 = color => {
- updateWizardData({
- selectedColor: color,
- finalAgent: {
- agentType: wizardData.agentType,
- whenToUse: wizardData.whenToUse,
- getSystemPrompt: () => wizardData.systemPrompt,
- tools: wizardData.selectedTools,
- ...(wizardData.selectedModel ? {
- model: wizardData.selectedModel
- } : {}),
- ...(color ? {
- color: color as AgentColorName
- } : {}),
- source: wizardData.location
- }
- });
- goNext();
- };
- $[1] = goNext;
- $[2] = updateWizardData;
- $[3] = wizardData.agentType;
- $[4] = wizardData.location;
- $[5] = wizardData.selectedModel;
- $[6] = wizardData.selectedTools;
- $[7] = wizardData.systemPrompt;
- $[8] = wizardData.whenToUse;
- $[9] = t1;
- } else {
- t1 = $[9];
- }
- const handleConfirm = t1;
- let t2;
- if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
- t2 = ;
- $[10] = t2;
- } else {
- t2 = $[10];
- }
- const t3 = wizardData.agentType || "agent";
- let t4;
- if ($[11] !== handleConfirm || $[12] !== t3) {
- t4 = ;
- $[11] = handleConfirm;
- $[12] = t3;
- $[13] = t4;
- } else {
- t4 = $[13];
- }
- return t4;
+
+ return (
+
+
+
+
+
+ }
+ >
+
+
+
+
+ )
}
diff --git a/src/components/agents/new-agent-creation/wizard-steps/ConfirmStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/ConfirmStep.tsx
index b696d861b..bfa035eb5 100644
--- a/src/components/agents/new-agent-creation/wizard-steps/ConfirmStep.tsx
+++ b/src/components/agents/new-agent-creation/wizard-steps/ConfirmStep.tsx
@@ -1,377 +1,168 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { type ReactNode } from 'react';
-import type { KeyboardEvent } from '../../../../ink/events/keyboard-event.js';
-import { Box, Text } from '../../../../ink.js';
-import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
-import { isAutoMemoryEnabled } from '../../../../memdir/paths.js';
-import type { Tools } from '../../../../Tool.js';
-import { getMemoryScopeDisplay } from '../../../../tools/AgentTool/agentMemory.js';
-import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js';
-import { truncateToWidth } from '../../../../utils/format.js';
-import { getAgentModelDisplay } from '../../../../utils/model/agent.js';
-import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
-import { Byline } from '../../../design-system/Byline.js';
-import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
-import { useWizard } from '../../../wizard/index.js';
-import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
-import { getNewRelativeAgentFilePath } from '../../agentFileUtils.js';
-import { validateAgent } from '../../validateAgent.js';
-import type { AgentWizardData } from '../types.js';
+import React, { type ReactNode } from 'react'
+import type { KeyboardEvent } from '../../../../ink/events/keyboard-event.js'
+import { Box, Text } from '../../../../ink.js'
+import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
+import { isAutoMemoryEnabled } from '../../../../memdir/paths.js'
+import type { Tools } from '../../../../Tool.js'
+import { getMemoryScopeDisplay } from '../../../../tools/AgentTool/agentMemory.js'
+import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'
+import { truncateToWidth } from '../../../../utils/format.js'
+import { getAgentModelDisplay } from '../../../../utils/model/agent.js'
+import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
+import { Byline } from '../../../design-system/Byline.js'
+import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
+import { useWizard } from '../../../wizard/index.js'
+import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
+import { getNewRelativeAgentFilePath } from '../../agentFileUtils.js'
+import { validateAgent } from '../../validateAgent.js'
+import type { AgentWizardData } from '../types.js'
+
type Props = {
- tools: Tools;
- existingAgents: AgentDefinition[];
- onSave: () => void;
- onSaveAndEdit: () => void;
- error?: string | null;
-};
-export function ConfirmStep(t0) {
- const $ = _c(88);
- const {
- tools,
- existingAgents,
- onSave,
- onSaveAndEdit,
- error
- } = t0;
- const {
- goBack,
- wizardData
- } = useWizard();
- let t1;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = {
- context: "Confirmation"
- };
- $[0] = t1;
- } else {
- t1 = $[0];
+ tools: Tools
+ existingAgents: AgentDefinition[]
+ onSave: () => void
+ onSaveAndEdit: () => void
+ error?: string | null
+}
+
+export function ConfirmStep({
+ tools,
+ existingAgents,
+ onSave,
+ onSaveAndEdit,
+ error,
+}: Props): ReactNode {
+ const { goBack, wizardData } = useWizard()
+
+ useKeybinding('confirm:no', goBack, { context: 'Confirmation' })
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 's' || e.key === 'return') {
+ e.preventDefault()
+ onSave()
+ } else if (e.key === 'e') {
+ e.preventDefault()
+ onSaveAndEdit()
+ }
}
- useKeybinding("confirm:no", goBack, t1);
- let t2;
- if ($[1] !== onSave || $[2] !== onSaveAndEdit) {
- t2 = e => {
- if (e.key === "s" || e.key === "return") {
- e.preventDefault();
- onSave();
- } else {
- if (e.key === "e") {
- e.preventDefault();
- onSaveAndEdit();
- }
+
+ const agent = wizardData.finalAgent!
+ const validation = validateAgent(agent, tools, existingAgents)
+
+ const systemPromptPreview = truncateToWidth(agent.getSystemPrompt(), 240)
+ const whenToUsePreview = truncateToWidth(agent.whenToUse, 240)
+
+ const getToolsDisplay = (toolNames: string[] | undefined): string => {
+ // undefined means "all tools" per PR semantic
+ if (toolNames === undefined) return 'All tools'
+ if (toolNames.length === 0) return 'None'
+ if (toolNames.length === 1) return toolNames[0] || 'None'
+ if (toolNames.length === 2) return toolNames.join(' and ')
+ return `${toolNames.slice(0, -1).join(', ')}, and ${toolNames[toolNames.length - 1]}`
+ }
+
+ // Compute memory display outside JSX
+ const memoryDisplayElement = isAutoMemoryEnabled() ? (
+
+ Memory: {getMemoryScopeDisplay(agent.memory)}
+
+ ) : null
+
+ return (
+
+
+
+
+
}
- };
- $[1] = onSave;
- $[2] = onSaveAndEdit;
- $[3] = t2;
- } else {
- t2 = $[3];
- }
- const handleKeyDown = t2;
- const agent = wizardData.finalAgent;
- let T0;
- let T1;
- let t10;
- let t11;
- let t12;
- let t13;
- let t14;
- let t15;
- let t16;
- let t17;
- let t18;
- let t19;
- let t3;
- let t4;
- let t5;
- let t6;
- let t7;
- let t8;
- let t9;
- if ($[4] !== agent || $[5] !== existingAgents || $[6] !== handleKeyDown || $[7] !== tools || $[8] !== wizardData.location) {
- const validation = validateAgent(agent, tools, existingAgents);
- let t20;
- if ($[28] !== agent) {
- t20 = truncateToWidth(agent.getSystemPrompt(), 240);
- $[28] = agent;
- $[29] = t20;
- } else {
- t20 = $[29];
- }
- const systemPromptPreview = t20;
- let t21;
- if ($[30] !== agent.whenToUse) {
- t21 = truncateToWidth(agent.whenToUse, 240);
- $[30] = agent.whenToUse;
- $[31] = t21;
- } else {
- t21 = $[31];
- }
- const whenToUsePreview = t21;
- const getToolsDisplay = _temp;
- let t22;
- if ($[32] !== agent.memory) {
- t22 = isAutoMemoryEnabled() ? Memory: {getMemoryScopeDisplay(agent.memory)} : null;
- $[32] = agent.memory;
- $[33] = t22;
- } else {
- t22 = $[33];
- }
- const memoryDisplayElement = t22;
- T1 = WizardDialogLayout;
- t18 = "Confirm and save";
- if ($[34] === Symbol.for("react.memo_cache_sentinel")) {
- t19 = ;
- $[34] = t19;
- } else {
- t19 = $[34];
- }
- T0 = Box;
- t3 = "column";
- t4 = 0;
- t5 = true;
- t6 = handleKeyDown;
- let t23;
- if ($[35] === Symbol.for("react.memo_cache_sentinel")) {
- t23 = Name;
- $[35] = t23;
- } else {
- t23 = $[35];
- }
- if ($[36] !== agent.agentType) {
- t7 = {t23}: {agent.agentType};
- $[36] = agent.agentType;
- $[37] = t7;
- } else {
- t7 = $[37];
- }
- let t24;
- if ($[38] === Symbol.for("react.memo_cache_sentinel")) {
- t24 = Location;
- $[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 = {t24}:{" "}{t25};
- $[42] = t25;
- $[43] = t8;
- } else {
- t8 = $[43];
- }
- let t26;
- if ($[44] === Symbol.for("react.memo_cache_sentinel")) {
- t26 = Tools;
- $[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 = {t26}: {t27};
- $[47] = t27;
- $[48] = t9;
- } else {
- t9 = $[48];
- }
- let t28;
- if ($[49] === Symbol.for("react.memo_cache_sentinel")) {
- t28 = Model;
- $[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 = {t28}: {t29};
- $[52] = t29;
- $[53] = t10;
- } else {
- t10 = $[53];
- }
- t11 = memoryDisplayElement;
- if ($[54] === Symbol.for("react.memo_cache_sentinel")) {
- t12 = Description (tells Claude when to use this agent):;
- $[54] = t12;
- } else {
- t12 = $[54];
- }
- if ($[55] !== whenToUsePreview) {
- t13 = {whenToUsePreview};
- $[55] = whenToUsePreview;
- $[56] = t13;
- } else {
- t13 = $[56];
- }
- if ($[57] === Symbol.for("react.memo_cache_sentinel")) {
- t14 = System prompt:;
- $[57] = t14;
- } else {
- t14 = $[57];
- }
- if ($[58] !== systemPromptPreview) {
- t15 = {systemPromptPreview};
- $[58] = systemPromptPreview;
- $[59] = t15;
- } else {
- t15 = $[59];
- }
- t16 = validation.warnings.length > 0 && Warnings:{validation.warnings.map(_temp2)};
- t17 = validation.errors.length > 0 && Errors:{validation.errors.map(_temp3)};
- $[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 && {error};
- $[60] = error;
- $[61] = t20;
- } else {
- t20 = $[61];
- }
- let t21;
- if ($[62] === Symbol.for("react.memo_cache_sentinel")) {
- t21 = s;
- $[62] = t21;
- } else {
- t21 = $[62];
- }
- let t22;
- if ($[63] === Symbol.for("react.memo_cache_sentinel")) {
- t22 = Enter;
- $[63] = t22;
- } else {
- t22 = $[63];
- }
- let t23;
- if ($[64] === Symbol.for("react.memo_cache_sentinel")) {
- t23 = Press {t21} or {t22} to save,{" "}e to save and edit;
- $[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 = {t7}{t8}{t9}{t10}{t11}{t12}{t13}{t14}{t15}{t16}{t17}{t20}{t23};
- $[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 = {t24};
- $[83] = T1;
- $[84] = t18;
- $[85] = t19;
- $[86] = t24;
- $[87] = t25;
- } else {
- t25 = $[87];
- }
- return t25;
-}
-function _temp3(err, i_0) {
- return {" "}• {err};
-}
-function _temp2(warning, i) {
- return {" "}• {warning};
-}
-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]}`;
+ >
+
+
+ Name: {agent.agentType}
+
+
+ Location:{' '}
+ {getNewRelativeAgentFilePath({
+ source: wizardData.location!,
+ agentType: agent.agentType,
+ })}
+
+
+ Tools: {getToolsDisplay(agent.tools)}
+
+
+ Model: {getAgentModelDisplay(agent.model)}
+
+ {memoryDisplayElement}
+
+
+
+ Description (tells Claude when to use this agent):
+
+
+
+ {whenToUsePreview}
+
+
+
+
+ System prompt:
+
+
+
+ {systemPromptPreview}
+
+
+ {validation.warnings.length > 0 && (
+
+ Warnings:
+ {validation.warnings.map((warning, i) => (
+
+ {' '}
+ • {warning}
+
+ ))}
+
+ )}
+
+ {validation.errors.length > 0 && (
+
+ Errors:
+ {validation.errors.map((err, i) => (
+
+ {' '}
+ • {err}
+
+ ))}
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ Press s or Enter to save,{' '}
+ e to save and edit
+
+
+
+
+ )
}
diff --git a/src/components/agents/new-agent-creation/wizard-steps/ConfirmStepWrapper.tsx b/src/components/agents/new-agent-creation/wizard-steps/ConfirmStepWrapper.tsx
index 0def7267b..013de633a 100644
--- a/src/components/agents/new-agent-creation/wizard-steps/ConfirmStepWrapper.tsx
+++ b/src/components/agents/new-agent-creation/wizard-steps/ConfirmStepWrapper.tsx
@@ -1,73 +1,112 @@
-import chalk from 'chalk';
-import React, { type ReactNode, useCallback, useState } from 'react';
-import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
-import { useSetAppState } from 'src/state/AppState.js';
-import type { Tools } from '../../../../Tool.js';
-import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js';
-import { getActiveAgentsFromList } from '../../../../tools/AgentTool/loadAgentsDir.js';
-import { editFileInEditor } from '../../../../utils/promptEditor.js';
-import { useWizard } from '../../../wizard/index.js';
-import { getNewAgentFilePath, saveAgentToFile } from '../../agentFileUtils.js';
-import type { AgentWizardData } from '../types.js';
-import { ConfirmStep } from './ConfirmStep.js';
+import chalk from 'chalk'
+import React, { type ReactNode, useCallback, useState } from 'react'
+import {
+ type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
+ logEvent,
+} from 'src/services/analytics/index.js'
+import { useSetAppState } from 'src/state/AppState.js'
+import type { Tools } from '../../../../Tool.js'
+import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'
+import { getActiveAgentsFromList } from '../../../../tools/AgentTool/loadAgentsDir.js'
+import { editFileInEditor } from '../../../../utils/promptEditor.js'
+import { useWizard } from '../../../wizard/index.js'
+import { getNewAgentFilePath, saveAgentToFile } from '../../agentFileUtils.js'
+import type { AgentWizardData } from '../types.js'
+import { ConfirmStep } from './ConfirmStep.js'
+
type Props = {
- tools: Tools;
- existingAgents: AgentDefinition[];
- onComplete: (message: string) => void;
-};
+ tools: Tools
+ existingAgents: AgentDefinition[]
+ onComplete: (message: string) => void
+}
+
export function ConfirmStepWrapper({
tools,
existingAgents,
- onComplete
+ onComplete,
}: Props): ReactNode {
- const {
- wizardData
- } = useWizard();
- const [saveError, setSaveError] = useState(null);
- const setAppState = useSetAppState();
- const saveAgent = useCallback(async (openInEditor: boolean): Promise => {
- if (!wizardData?.finalAgent) return;
- try {
- await saveAgentToFile(wizardData.location!, wizardData.finalAgent.agentType, wizardData.finalAgent.whenToUse, wizardData.finalAgent.tools, wizardData.finalAgent.getSystemPrompt(), true, wizardData.finalAgent.color, wizardData.finalAgent.model, wizardData.finalAgent.memory);
- setAppState(state => {
- if (!wizardData.finalAgent) return state;
- const allAgents = state.agentDefinitions.allAgents.concat(wizardData.finalAgent);
- return {
- ...state,
- agentDefinitions: {
- ...state.agentDefinitions,
- activeAgents: getActiveAgentsFromList(allAgents),
- allAgents
+ const { wizardData } = useWizard()
+ const [saveError, setSaveError] = useState(null)
+ const setAppState = useSetAppState()
+
+ const saveAgent = useCallback(
+ async (openInEditor: boolean): Promise => {
+ if (!wizardData?.finalAgent) return
+
+ try {
+ await saveAgentToFile(
+ wizardData.location!,
+ wizardData.finalAgent.agentType,
+ wizardData.finalAgent.whenToUse,
+ wizardData.finalAgent.tools,
+ wizardData.finalAgent.getSystemPrompt(),
+ true,
+ wizardData.finalAgent.color,
+ wizardData.finalAgent.model,
+ wizardData.finalAgent.memory,
+ )
+
+ setAppState(state => {
+ if (!wizardData.finalAgent) return state
+
+ const allAgents = state.agentDefinitions.allAgents.concat(
+ wizardData.finalAgent,
+ )
+ return {
+ ...state,
+ agentDefinitions: {
+ ...state.agentDefinitions,
+ activeAgents: getActiveAgentsFromList(allAgents),
+ allAgents,
+ },
}
- };
- });
- if (openInEditor) {
- const filePath = getNewAgentFilePath({
+ })
+
+ if (openInEditor) {
+ const filePath = getNewAgentFilePath({
+ source: wizardData.location!,
+ agentType: wizardData.finalAgent.agentType,
+ })
+ await editFileInEditor(filePath)
+ }
+
+ logEvent('tengu_agent_created', {
+ agent_type: wizardData.finalAgent.agentType,
+ generation_method: wizardData.wasGenerated ? 'generated' : 'manual',
source: wizardData.location!,
- agentType: wizardData.finalAgent.agentType
- });
- await editFileInEditor(filePath);
+ tool_count: wizardData.finalAgent.tools?.length ?? 'all',
+ has_custom_model: !!wizardData.finalAgent.model,
+ has_custom_color: !!wizardData.finalAgent.color,
+ has_memory: !!wizardData.finalAgent.memory,
+ memory_scope: wizardData.finalAgent.memory ?? 'none',
+ ...(openInEditor ? { opened_in_editor: true } : {}),
+ } as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS)
+
+ const message = openInEditor
+ ? `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)} and opened in editor. ` +
+ `If you made edits, restart to load the latest version.`
+ : `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)}`
+ onComplete(message)
+ } catch (err) {
+ setSaveError(
+ err instanceof Error ? err.message : 'Failed to save agent',
+ )
}
- logEvent('tengu_agent_created', {
- agent_type: wizardData.finalAgent.agentType,
- generation_method: wizardData.wasGenerated ? 'generated' : 'manual',
- source: wizardData.location!,
- tool_count: wizardData.finalAgent.tools?.length ?? 'all',
- has_custom_model: !!wizardData.finalAgent.model,
- has_custom_color: !!wizardData.finalAgent.color,
- has_memory: !!wizardData.finalAgent.memory,
- memory_scope: wizardData.finalAgent.memory ?? 'none',
- ...(openInEditor ? {
- opened_in_editor: true
- } : {})
- } as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS);
- const message = openInEditor ? `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)} and opened in editor. ` + `If you made edits, restart to load the latest version.` : `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)}`;
- onComplete(message);
- } catch (err) {
- setSaveError(err instanceof Error ? err.message : 'Failed to save agent');
- }
- }, [wizardData, onComplete, setAppState]);
- const handleSave = useCallback(() => saveAgent(false), [saveAgent]);
- const handleSaveAndEdit = useCallback(() => saveAgent(true), [saveAgent]);
- return ;
+ },
+ [wizardData, onComplete, setAppState],
+ )
+
+ const handleSave = useCallback(() => saveAgent(false), [saveAgent])
+
+ const handleSaveAndEdit = useCallback(() => saveAgent(true), [saveAgent])
+
+ return (
+
+ )
}
diff --git a/src/components/agents/new-agent-creation/wizard-steps/DescriptionStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/DescriptionStep.tsx
index 504ff0fd1..1138cc3d3 100644
--- a/src/components/agents/new-agent-creation/wizard-steps/DescriptionStep.tsx
+++ b/src/components/agents/new-agent-creation/wizard-steps/DescriptionStep.tsx
@@ -1,122 +1,94 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { type ReactNode, useCallback, useState } from 'react';
-import { Box, Text } from '../../../../ink.js';
-import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
-import { editPromptInEditor } from '../../../../utils/promptEditor.js';
-import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
-import { Byline } from '../../../design-system/Byline.js';
-import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
-import TextInput from '../../../TextInput.js';
-import { useWizard } from '../../../wizard/index.js';
-import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
-import type { AgentWizardData } from '../types.js';
-export function DescriptionStep() {
- const $ = _c(18);
- const {
- goNext,
- goBack,
- updateWizardData,
- wizardData
- } = useWizard();
- const [whenToUse, setWhenToUse] = useState(wizardData.whenToUse || "");
- const [cursorOffset, setCursorOffset] = useState(whenToUse.length);
- const [error, setError] = useState(null);
- let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t0 = {
- context: "Settings"
- };
- $[0] = t0;
- } else {
- t0 = $[0];
+import React, { type ReactNode, useCallback, useState } from 'react'
+import { Box, Text } from '../../../../ink.js'
+import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
+import { editPromptInEditor } from '../../../../utils/promptEditor.js'
+import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
+import { Byline } from '../../../design-system/Byline.js'
+import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
+import TextInput from '../../../TextInput.js'
+import { useWizard } from '../../../wizard/index.js'
+import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
+import type { AgentWizardData } from '../types.js'
+
+export function DescriptionStep(): ReactNode {
+ const { goNext, goBack, updateWizardData, wizardData } =
+ useWizard()
+ const [whenToUse, setWhenToUse] = useState(wizardData.whenToUse || '')
+ const [cursorOffset, setCursorOffset] = useState(whenToUse.length)
+ const [error, setError] = useState(null)
+
+ // Handle escape key - use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
+ useKeybinding('confirm:no', goBack, { context: 'Settings' })
+
+ const handleExternalEditor = useCallback(async () => {
+ const result = await editPromptInEditor(whenToUse)
+ if (result.content !== null) {
+ setWhenToUse(result.content)
+ setCursorOffset(result.content.length)
+ }
+ }, [whenToUse])
+
+ useKeybinding('chat:externalEditor', handleExternalEditor, {
+ context: 'Chat',
+ })
+
+ const handleSubmit = (value: string): void => {
+ const trimmedValue = value.trim()
+ if (!trimmedValue) {
+ setError('Description is required')
+ return
+ }
+
+ setError(null)
+ updateWizardData({ whenToUse: trimmedValue })
+ goNext()
}
- useKeybinding("confirm:no", goBack, t0);
- let t1;
- if ($[1] !== whenToUse) {
- t1 = async () => {
- const result = await editPromptInEditor(whenToUse);
- if (result.content !== null) {
- setWhenToUse(result.content);
- setCursorOffset(result.content.length);
+
+ return (
+
+
+
+
+
+
}
- };
- $[1] = whenToUse;
- $[2] = t1;
- } else {
- t1 = $[2];
- }
- const handleExternalEditor = t1;
- let t2;
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
- t2 = {
- context: "Chat"
- };
- $[3] = t2;
- } else {
- t2 = $[3];
- }
- useKeybinding("chat:externalEditor", handleExternalEditor, t2);
- let t3;
- if ($[4] !== goNext || $[5] !== updateWizardData) {
- t3 = value => {
- const trimmedValue = value.trim();
- if (!trimmedValue) {
- setError("Description is required");
- return;
- }
- setError(null);
- updateWizardData({
- whenToUse: trimmedValue
- });
- goNext();
- };
- $[4] = goNext;
- $[5] = updateWizardData;
- $[6] = t3;
- } else {
- t3 = $[6];
- }
- const handleSubmit = t3;
- let t4;
- if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
- t4 = ;
- $[7] = t4;
- } else {
- t4 = $[7];
- }
- let t5;
- if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
- t5 = When should Claude use this agent?;
- $[8] = t5;
- } else {
- t5 = $[8];
- }
- let t6;
- if ($[9] !== cursorOffset || $[10] !== handleSubmit || $[11] !== whenToUse) {
- t6 = ;
- $[9] = cursorOffset;
- $[10] = handleSubmit;
- $[11] = whenToUse;
- $[12] = t6;
- } else {
- t6 = $[12];
- }
- let t7;
- if ($[13] !== error) {
- t7 = error && {error};
- $[13] = error;
- $[14] = t7;
- } else {
- t7 = $[14];
- }
- let t8;
- if ($[15] !== t6 || $[16] !== t7) {
- t8 = {t5}{t6}{t7};
- $[15] = t6;
- $[16] = t7;
- $[17] = t8;
- } else {
- t8 = $[17];
- }
- return t8;
+ >
+
+ When should Claude use this agent?
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ )
}
diff --git a/src/components/agents/new-agent-creation/wizard-steps/GenerateStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/GenerateStep.tsx
index 892833bc3..1cb7ae69d 100644
--- a/src/components/agents/new-agent-creation/wizard-steps/GenerateStep.tsx
+++ b/src/components/agents/new-agent-creation/wizard-steps/GenerateStep.tsx
@@ -1,58 +1,57 @@
-import { APIUserAbortError } from '@anthropic-ai/sdk';
-import React, { type ReactNode, useCallback, useRef, useState } from 'react';
-import { useMainLoopModel } from '../../../../hooks/useMainLoopModel.js';
-import { Box, Text } from '../../../../ink.js';
-import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
-import { createAbortController } from '../../../../utils/abortController.js';
-import { editPromptInEditor } from '../../../../utils/promptEditor.js';
-import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
-import { Byline } from '../../../design-system/Byline.js';
-import { Spinner } from '../../../Spinner.js';
-import TextInput from '../../../TextInput.js';
-import { useWizard } from '../../../wizard/index.js';
-import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
-import { generateAgent } from '../../generateAgent.js';
-import type { AgentWizardData } from '../types.js';
+import { APIUserAbortError } from '@anthropic-ai/sdk'
+import React, { type ReactNode, useCallback, useRef, useState } from 'react'
+import { useMainLoopModel } from '../../../../hooks/useMainLoopModel.js'
+import { Box, Text } from '../../../../ink.js'
+import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
+import { createAbortController } from '../../../../utils/abortController.js'
+import { editPromptInEditor } from '../../../../utils/promptEditor.js'
+import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
+import { Byline } from '../../../design-system/Byline.js'
+import { Spinner } from '../../../Spinner.js'
+import TextInput from '../../../TextInput.js'
+import { useWizard } from '../../../wizard/index.js'
+import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
+import { generateAgent } from '../../generateAgent.js'
+import type { AgentWizardData } from '../types.js'
+
export function GenerateStep(): ReactNode {
- const {
- updateWizardData,
- goBack,
- goToStep,
- wizardData
- } = useWizard();
- const [prompt, setPrompt] = useState(wizardData.generationPrompt || '');
- const [isGenerating, setIsGenerating] = useState(false);
- const [error, setError] = useState(null);
- const [cursorOffset, setCursorOffset] = useState(prompt.length);
- const model = useMainLoopModel();
- const abortControllerRef = useRef(null);
+ const { updateWizardData, goBack, goToStep, wizardData } =
+ useWizard()
+ const [prompt, setPrompt] = useState(wizardData.generationPrompt || '')
+ const [isGenerating, setIsGenerating] = useState(false)
+ const [error, setError] = useState(null)
+ const [cursorOffset, setCursorOffset] = useState(prompt.length)
+ const model = useMainLoopModel()
+ const abortControllerRef = useRef(null)
// Cancel generation when escape pressed during generation
const handleCancelGeneration = useCallback(() => {
if (abortControllerRef.current) {
- abortControllerRef.current.abort();
- abortControllerRef.current = null;
- setIsGenerating(false);
- setError('Generation cancelled');
+ abortControllerRef.current.abort()
+ abortControllerRef.current = null
+ setIsGenerating(false)
+ setError('Generation cancelled')
}
- }, []);
+ }, [])
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in prompt input)
useKeybinding('confirm:no', handleCancelGeneration, {
context: 'Settings',
- isActive: isGenerating
- });
+ isActive: isGenerating,
+ })
+
const handleExternalEditor = useCallback(async () => {
- const result = await editPromptInEditor(prompt);
+ const result = await editPromptInEditor(prompt)
if (result.content !== null) {
- setPrompt(result.content);
- setCursorOffset(result.content.length);
+ setPrompt(result.content)
+ setCursorOffset(result.content.length)
}
- }, [prompt]);
+ }, [prompt])
+
useKeybinding('chat:externalEditor', handleExternalEditor, {
context: 'Chat',
- isActive: !isGenerating
- });
+ isActive: !isGenerating,
+ })
// Go back when escape pressed while not generating
const handleGoBack = useCallback(() => {
@@ -62,81 +61,141 @@ export function GenerateStep(): ReactNode {
systemPrompt: '',
whenToUse: '',
generatedAgent: undefined,
- wasGenerated: false
- });
- setPrompt('');
- setError(null);
- goBack();
- }, [updateWizardData, goBack]);
+ wasGenerated: false,
+ })
+ setPrompt('')
+ setError(null)
+ goBack()
+ }, [updateWizardData, goBack])
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in prompt input)
useKeybinding('confirm:no', handleGoBack, {
context: 'Settings',
- isActive: !isGenerating
- });
+ isActive: !isGenerating,
+ })
+
const handleGenerate = async (): Promise => {
- const trimmedPrompt = prompt.trim();
+ const trimmedPrompt = prompt.trim()
if (!trimmedPrompt) {
- setError('Please describe what the agent should do');
- return;
+ setError('Please describe what the agent should do')
+ return
}
- setError(null);
- setIsGenerating(true);
+
+ setError(null)
+ setIsGenerating(true)
updateWizardData({
generationPrompt: trimmedPrompt,
- isGenerating: true
- });
+ isGenerating: true,
+ })
// Create abort controller for this generation
- const controller = createAbortController();
- abortControllerRef.current = controller;
+ const controller = createAbortController()
+ abortControllerRef.current = controller
+
try {
- const generated = await generateAgent(trimmedPrompt, model, [], controller.signal);
+ const generated = await generateAgent(
+ trimmedPrompt,
+ model,
+ [],
+ controller.signal,
+ )
+
updateWizardData({
agentType: generated.identifier,
whenToUse: generated.whenToUse,
systemPrompt: generated.systemPrompt,
generatedAgent: generated,
isGenerating: false,
- wasGenerated: true
- });
+ wasGenerated: true,
+ })
// Skip directly to ToolsStep (index 6) - matching original flow
- goToStep(6);
+ goToStep(6)
} catch (err) {
// Don't show error if it was cancelled (already set in escape handler)
if (err instanceof APIUserAbortError) {
// User cancelled - no error to show
- } else if (err instanceof Error && !err.message.includes('No assistant message found')) {
- setError(err.message || 'Failed to generate agent');
+ } else if (
+ err instanceof Error &&
+ !err.message.includes('No assistant message found')
+ ) {
+ setError(err.message || 'Failed to generate agent')
}
- updateWizardData({
- isGenerating: false
- });
+ updateWizardData({ isGenerating: false })
} finally {
- setIsGenerating(false);
- abortControllerRef.current = null;
+ setIsGenerating(false)
+ abortControllerRef.current = null
}
- };
- const subtitle = 'Describe what this agent should do and when it should be used (be comprehensive for best results)';
+ }
+
+ const subtitle =
+ 'Describe what this agent should do and when it should be used (be comprehensive for best results)'
+
if (isGenerating) {
- return }>
+ return (
+
+ }
+ >
Generating agent from description...
- ;
+
+ )
}
- return
-
-
-
- }>
+
+ return (
+
+
+
+
+
+ }
+ >
- {error &&
+ {error && (
+
{error}
- }
-
+
+ )}
+
- ;
+
+ )
}
diff --git a/src/components/agents/new-agent-creation/wizard-steps/LocationStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/LocationStep.tsx
index cf0a544d5..a7fd0a2bc 100644
--- a/src/components/agents/new-agent-creation/wizard-steps/LocationStep.tsx
+++ b/src/components/agents/new-agent-creation/wizard-steps/LocationStep.tsx
@@ -1,79 +1,55 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { type ReactNode } from 'react';
-import { Box } from '../../../../ink.js';
-import type { SettingSource } from '../../../../utils/settings/constants.js';
-import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
-import { Select } from '../../../CustomSelect/select.js';
-import { Byline } from '../../../design-system/Byline.js';
-import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
-import { useWizard } from '../../../wizard/index.js';
-import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
-import type { AgentWizardData } from '../types.js';
-export function LocationStep() {
- const $ = _c(11);
- const {
- goNext,
- updateWizardData,
- cancel
- } = useWizard();
- let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t0 = {
- label: "Project (.claude/agents/)",
- value: "projectSettings" as SettingSource
- };
- $[0] = t0;
- } else {
- t0 = $[0];
- }
- let t1;
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = [t0, {
- label: "Personal (~/.claude/agents/)",
- value: "userSettings" as SettingSource
- }];
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- const locationOptions = t1;
- let t2;
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
- t2 = ;
- $[2] = t2;
- } else {
- t2 = $[2];
- }
- let t3;
- if ($[3] !== goNext || $[4] !== updateWizardData) {
- t3 = value => {
- updateWizardData({
- location: value as SettingSource
- });
- goNext();
- };
- $[3] = goNext;
- $[4] = updateWizardData;
- $[5] = t3;
- } else {
- t3 = $[5];
- }
- let t4;
- if ($[6] !== cancel) {
- t4 = () => cancel();
- $[6] = cancel;
- $[7] = t4;
- } else {
- t4 = $[7];
- }
- let t5;
- if ($[8] !== t3 || $[9] !== t4) {
- t5 = ;
- $[8] = t3;
- $[9] = t4;
- $[10] = t5;
- } else {
- t5 = $[10];
- }
- return t5;
+import React, { type ReactNode } from 'react'
+import { Box } from '../../../../ink.js'
+import type { SettingSource } from '../../../../utils/settings/constants.js'
+import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
+import { Select } from '../../../CustomSelect/select.js'
+import { Byline } from '../../../design-system/Byline.js'
+import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
+import { useWizard } from '../../../wizard/index.js'
+import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
+import type { AgentWizardData } from '../types.js'
+
+export function LocationStep(): ReactNode {
+ const { goNext, updateWizardData, cancel } = useWizard()
+
+ const locationOptions = [
+ {
+ label: 'Project (.claude/agents/)',
+ value: 'projectSettings' as SettingSource,
+ },
+ {
+ label: 'Personal (~/.claude/agents/)',
+ value: 'userSettings' as SettingSource,
+ },
+ ]
+
+ return (
+
+
+
+
+
+ }
+ >
+
+
+
+ )
}
diff --git a/src/components/agents/new-agent-creation/wizard-steps/MemoryStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/MemoryStep.tsx
index fc5cad0f3..3c987cf77 100644
--- a/src/components/agents/new-agent-creation/wizard-steps/MemoryStep.tsx
+++ b/src/components/agents/new-agent-creation/wizard-steps/MemoryStep.tsx
@@ -1,112 +1,102 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { type ReactNode } from 'react';
-import { Box } from '../../../../ink.js';
-import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
-import { isAutoMemoryEnabled } from '../../../../memdir/paths.js';
-import { type AgentMemoryScope, loadAgentMemoryPrompt } from '../../../../tools/AgentTool/agentMemory.js';
-import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
-import { Select } from '../../../CustomSelect/select.js';
-import { Byline } from '../../../design-system/Byline.js';
-import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
-import { useWizard } from '../../../wizard/index.js';
-import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
-import type { AgentWizardData } from '../types.js';
+import React, { type ReactNode } from 'react'
+import { Box } from '../../../../ink.js'
+import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
+import { isAutoMemoryEnabled } from '../../../../memdir/paths.js'
+import {
+ type AgentMemoryScope,
+ loadAgentMemoryPrompt,
+} from '../../../../tools/AgentTool/agentMemory.js'
+import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
+import { Select } from '../../../CustomSelect/select.js'
+import { Byline } from '../../../design-system/Byline.js'
+import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
+import { useWizard } from '../../../wizard/index.js'
+import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
+import type { AgentWizardData } from '../types.js'
+
type MemoryOption = {
- label: string;
- value: AgentMemoryScope | 'none';
-};
-export function MemoryStep() {
- const $ = _c(13);
- const {
- goNext,
- goBack,
- updateWizardData,
- wizardData
- } = useWizard();
- let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t0 = {
- context: "Confirmation"
- };
- $[0] = t0;
- } else {
- t0 = $[0];
- }
- useKeybinding("confirm:no", goBack, t0);
- const isUserScope = wizardData.location === "userSettings";
- let t1;
- if ($[1] !== isUserScope) {
- t1 = isUserScope ? [{
- label: "User scope (~/.claude/agent-memory/) (Recommended)",
- value: "user"
- }, {
- label: "None (no persistent memory)",
- value: "none"
- }, {
- label: "Project scope (.claude/agent-memory/)",
- value: "project"
- }, {
- label: "Local scope (.claude/agent-memory-local/)",
- value: "local"
- }] : [{
- label: "Project scope (.claude/agent-memory/) (Recommended)",
- value: "project"
- }, {
- label: "None (no persistent memory)",
- value: "none"
- }, {
- label: "User scope (~/.claude/agent-memory/)",
- value: "user"
- }, {
- label: "Local scope (.claude/agent-memory-local/)",
- value: "local"
- }];
- $[1] = isUserScope;
- $[2] = t1;
- } else {
- t1 = $[2];
- }
- const memoryOptions = t1;
- let t2;
- if ($[3] !== goNext || $[4] !== updateWizardData || $[5] !== wizardData.finalAgent || $[6] !== wizardData.systemPrompt) {
- t2 = value => {
- const memory = value === "none" ? undefined : value as AgentMemoryScope;
- const agentType = wizardData.finalAgent?.agentType;
- updateWizardData({
- selectedMemory: memory,
- finalAgent: wizardData.finalAgent ? {
- ...wizardData.finalAgent,
- memory,
- getSystemPrompt: isAutoMemoryEnabled() && memory && agentType ? () => wizardData.systemPrompt + "\n\n" + loadAgentMemoryPrompt(agentType, memory) : () => wizardData.systemPrompt
- } : undefined
- });
- goNext();
- };
- $[3] = goNext;
- $[4] = updateWizardData;
- $[5] = wizardData.finalAgent;
- $[6] = wizardData.systemPrompt;
- $[7] = t2;
- } else {
- t2 = $[7];
- }
- const handleSelect = t2;
- let t3;
- if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
- t3 = ;
- $[8] = t3;
- } else {
- t3 = $[8];
- }
- let t4;
- if ($[9] !== goBack || $[10] !== handleSelect || $[11] !== memoryOptions) {
- t4 = ;
- $[9] = goBack;
- $[10] = handleSelect;
- $[11] = memoryOptions;
- $[12] = t4;
- } else {
- t4 = $[12];
- }
- return t4;
+ label: string
+ value: AgentMemoryScope | 'none'
+}
+
+export function MemoryStep(): ReactNode {
+ const { goNext, goBack, updateWizardData, wizardData } =
+ useWizard()
+
+ useKeybinding('confirm:no', goBack, { context: 'Confirmation' })
+
+ const isUserScope = wizardData.location === 'userSettings'
+
+ // Build options with the recommended default first, then alternatives
+ // The recommended scope matches the agent's location (project agent → project memory, user agent → user memory)
+ const memoryOptions: MemoryOption[] = isUserScope
+ ? [
+ {
+ label: 'User scope (~/.claude/agent-memory/) (Recommended)',
+ value: 'user',
+ },
+ { label: 'None (no persistent memory)', value: 'none' },
+ { label: 'Project scope (.claude/agent-memory/)', value: 'project' },
+ { label: 'Local scope (.claude/agent-memory-local/)', value: 'local' },
+ ]
+ : [
+ {
+ label: 'Project scope (.claude/agent-memory/) (Recommended)',
+ value: 'project',
+ },
+ { label: 'None (no persistent memory)', value: 'none' },
+ { label: 'User scope (~/.claude/agent-memory/)', value: 'user' },
+ { label: 'Local scope (.claude/agent-memory-local/)', value: 'local' },
+ ]
+
+ const handleSelect = (value: string): void => {
+ const memory = value === 'none' ? undefined : (value as AgentMemoryScope)
+ const agentType = wizardData.finalAgent?.agentType
+ updateWizardData({
+ selectedMemory: memory,
+ // Update finalAgent with memory and rewire getSystemPrompt to include memory loading.
+ // Explicitly set memory (not conditional spread) so selecting 'none' after going back clears it.
+ finalAgent: wizardData.finalAgent
+ ? {
+ ...wizardData.finalAgent,
+ memory,
+ getSystemPrompt:
+ isAutoMemoryEnabled() && memory && agentType
+ ? () =>
+ wizardData.systemPrompt! +
+ '\n\n' +
+ loadAgentMemoryPrompt(agentType, memory)
+ : () => wizardData.systemPrompt!,
+ }
+ : undefined,
+ })
+ goNext()
+ }
+
+ return (
+
+
+
+
+
+ }
+ >
+
+
+
+
+ )
}
diff --git a/src/components/agents/new-agent-creation/wizard-steps/MethodStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/MethodStep.tsx
index 5e9f40418..8f8252e12 100644
--- a/src/components/agents/new-agent-creation/wizard-steps/MethodStep.tsx
+++ b/src/components/agents/new-agent-creation/wizard-steps/MethodStep.tsx
@@ -1,79 +1,65 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { type ReactNode } from 'react';
-import { Box } from '../../../../ink.js';
-import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
-import { Select } from '../../../CustomSelect/select.js';
-import { Byline } from '../../../design-system/Byline.js';
-import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
-import { useWizard } from '../../../wizard/index.js';
-import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
-import type { AgentWizardData } from '../types.js';
-export function MethodStep() {
- const $ = _c(11);
- const {
- goNext,
- goBack,
- updateWizardData,
- goToStep
- } = useWizard();
- let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t0 = [{
- label: "Generate with Claude (recommended)",
- value: "generate"
- }, {
- label: "Manual configuration",
- value: "manual"
- }];
- $[0] = t0;
- } else {
- t0 = $[0];
- }
- const methodOptions = t0;
- let t1;
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = ;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- let t2;
- if ($[2] !== goNext || $[3] !== goToStep || $[4] !== updateWizardData) {
- t2 = value => {
- const method = value as 'generate' | 'manual';
- updateWizardData({
- method,
- wasGenerated: method === "generate"
- });
- if (method === "generate") {
- goNext();
- } else {
- goToStep(3);
+import React, { type ReactNode } from 'react'
+import { Box } from '../../../../ink.js'
+import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
+import { Select } from '../../../CustomSelect/select.js'
+import { Byline } from '../../../design-system/Byline.js'
+import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
+import { useWizard } from '../../../wizard/index.js'
+import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
+import type { AgentWizardData } from '../types.js'
+
+export function MethodStep(): ReactNode {
+ const { goNext, goBack, updateWizardData, goToStep } =
+ useWizard()
+
+ const methodOptions = [
+ {
+ label: 'Generate with Claude (recommended)',
+ value: 'generate',
+ },
+ {
+ label: 'Manual configuration',
+ value: 'manual',
+ },
+ ]
+
+ return (
+
+
+
+
+
}
- };
- $[2] = goNext;
- $[3] = goToStep;
- $[4] = updateWizardData;
- $[5] = t2;
- } else {
- t2 = $[5];
- }
- let t3;
- if ($[6] !== goBack) {
- t3 = () => goBack();
- $[6] = goBack;
- $[7] = t3;
- } else {
- t3 = $[7];
- }
- let t4;
- if ($[8] !== t2 || $[9] !== t3) {
- t4 = ;
- $[8] = t2;
- $[9] = t3;
- $[10] = t4;
- } else {
- t4 = $[10];
- }
- return t4;
+ >
+
+
+
+ )
}
diff --git a/src/components/agents/new-agent-creation/wizard-steps/ModelStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/ModelStep.tsx
index b53ffd683..586cc6cc8 100644
--- a/src/components/agents/new-agent-creation/wizard-steps/ModelStep.tsx
+++ b/src/components/agents/new-agent-creation/wizard-steps/ModelStep.tsx
@@ -1,51 +1,42 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { type ReactNode } from 'react';
-import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
-import { Byline } from '../../../design-system/Byline.js';
-import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
-import { useWizard } from '../../../wizard/index.js';
-import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
-import { ModelSelector } from '../../ModelSelector.js';
-import type { AgentWizardData } from '../types.js';
-export function ModelStep() {
- const $ = _c(8);
- const {
- goNext,
- goBack,
- updateWizardData,
- wizardData
- } = useWizard();
- let t0;
- if ($[0] !== goNext || $[1] !== updateWizardData) {
- t0 = model => {
- updateWizardData({
- selectedModel: model
- });
- goNext();
- };
- $[0] = goNext;
- $[1] = updateWizardData;
- $[2] = t0;
- } else {
- t0 = $[2];
+import React, { type ReactNode } from 'react'
+import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
+import { Byline } from '../../../design-system/Byline.js'
+import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
+import { useWizard } from '../../../wizard/index.js'
+import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
+import { ModelSelector } from '../../ModelSelector.js'
+import type { AgentWizardData } from '../types.js'
+
+export function ModelStep(): ReactNode {
+ const { goNext, goBack, updateWizardData, wizardData } =
+ useWizard()
+
+ const handleComplete = (model?: string): void => {
+ updateWizardData({ selectedModel: model })
+ goNext()
}
- const handleComplete = t0;
- let t1;
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = ;
- $[3] = t1;
- } else {
- t1 = $[3];
- }
- let t2;
- if ($[4] !== goBack || $[5] !== handleComplete || $[6] !== wizardData.selectedModel) {
- t2 = ;
- $[4] = goBack;
- $[5] = handleComplete;
- $[6] = wizardData.selectedModel;
- $[7] = t2;
- } else {
- t2 = $[7];
- }
- return t2;
+
+ return (
+
+
+
+
+
+ }
+ >
+
+
+ )
}
diff --git a/src/components/agents/new-agent-creation/wizard-steps/PromptStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/PromptStep.tsx
index 1b8224c28..4d6747520 100644
--- a/src/components/agents/new-agent-creation/wizard-steps/PromptStep.tsx
+++ b/src/components/agents/new-agent-creation/wizard-steps/PromptStep.tsx
@@ -1,127 +1,97 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { type ReactNode, useCallback, useState } from 'react';
-import { Box, Text } from '../../../../ink.js';
-import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
-import { editPromptInEditor } from '../../../../utils/promptEditor.js';
-import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
-import { Byline } from '../../../design-system/Byline.js';
-import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
-import TextInput from '../../../TextInput.js';
-import { useWizard } from '../../../wizard/index.js';
-import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
-import type { AgentWizardData } from '../types.js';
-export function PromptStep() {
- const $ = _c(20);
- const {
- goNext,
- goBack,
- updateWizardData,
- wizardData
- } = useWizard();
- const [systemPrompt, setSystemPrompt] = useState(wizardData.systemPrompt || "");
- const [cursorOffset, setCursorOffset] = useState(systemPrompt.length);
- const [error, setError] = useState(null);
- let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t0 = {
- context: "Settings"
- };
- $[0] = t0;
- } else {
- t0 = $[0];
+import React, { type ReactNode, useCallback, useState } from 'react'
+import { Box, Text } from '../../../../ink.js'
+import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
+import { editPromptInEditor } from '../../../../utils/promptEditor.js'
+import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
+import { Byline } from '../../../design-system/Byline.js'
+import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
+import TextInput from '../../../TextInput.js'
+import { useWizard } from '../../../wizard/index.js'
+import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
+import type { AgentWizardData } from '../types.js'
+
+export function PromptStep(): ReactNode {
+ const { goNext, goBack, updateWizardData, wizardData } =
+ useWizard()
+ const [systemPrompt, setSystemPrompt] = useState(
+ wizardData.systemPrompt || '',
+ )
+ const [cursorOffset, setCursorOffset] = useState(systemPrompt.length)
+ const [error, setError] = useState(null)
+
+ // Handle escape key - use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
+ useKeybinding('confirm:no', goBack, { context: 'Settings' })
+
+ const handleExternalEditor = useCallback(async () => {
+ const result = await editPromptInEditor(systemPrompt)
+ if (result.content !== null) {
+ setSystemPrompt(result.content)
+ setCursorOffset(result.content.length)
+ }
+ }, [systemPrompt])
+
+ useKeybinding('chat:externalEditor', handleExternalEditor, {
+ context: 'Chat',
+ })
+
+ const handleSubmit = (): void => {
+ const trimmedPrompt = systemPrompt.trim()
+ if (!trimmedPrompt) {
+ setError('System prompt is required')
+ return
+ }
+
+ setError(null)
+ updateWizardData({ systemPrompt: trimmedPrompt })
+ goNext()
}
- useKeybinding("confirm:no", goBack, t0);
- let t1;
- if ($[1] !== systemPrompt) {
- t1 = async () => {
- const result = await editPromptInEditor(systemPrompt);
- if (result.content !== null) {
- setSystemPrompt(result.content);
- setCursorOffset(result.content.length);
+
+ return (
+
+
+
+
+
+
}
- };
- $[1] = systemPrompt;
- $[2] = t1;
- } else {
- t1 = $[2];
- }
- const handleExternalEditor = t1;
- let t2;
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
- t2 = {
- context: "Chat"
- };
- $[3] = t2;
- } else {
- t2 = $[3];
- }
- useKeybinding("chat:externalEditor", handleExternalEditor, t2);
- let t3;
- if ($[4] !== goNext || $[5] !== systemPrompt || $[6] !== updateWizardData) {
- t3 = () => {
- const trimmedPrompt = systemPrompt.trim();
- if (!trimmedPrompt) {
- setError("System prompt is required");
- return;
- }
- setError(null);
- updateWizardData({
- systemPrompt: trimmedPrompt
- });
- goNext();
- };
- $[4] = goNext;
- $[5] = systemPrompt;
- $[6] = updateWizardData;
- $[7] = t3;
- } else {
- t3 = $[7];
- }
- const handleSubmit = t3;
- let t4;
- if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
- t4 = ;
- $[8] = t4;
- } else {
- t4 = $[8];
- }
- let t5;
- let t6;
- if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
- t5 = Enter the system prompt for your agent:;
- t6 = Be comprehensive for best results;
- $[9] = t5;
- $[10] = t6;
- } else {
- t5 = $[9];
- t6 = $[10];
- }
- let t7;
- if ($[11] !== cursorOffset || $[12] !== handleSubmit || $[13] !== systemPrompt) {
- t7 = ;
- $[11] = cursorOffset;
- $[12] = handleSubmit;
- $[13] = systemPrompt;
- $[14] = t7;
- } else {
- t7 = $[14];
- }
- let t8;
- if ($[15] !== error) {
- t8 = error && {error};
- $[15] = error;
- $[16] = t8;
- } else {
- t8 = $[16];
- }
- let t9;
- if ($[17] !== t7 || $[18] !== t8) {
- t9 = {t5}{t6}{t7}{t8};
- $[17] = t7;
- $[18] = t8;
- $[19] = t9;
- } else {
- t9 = $[19];
- }
- return t9;
+ >
+
+ Enter the system prompt for your agent:
+ Be comprehensive for best results
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ )
}
diff --git a/src/components/agents/new-agent-creation/wizard-steps/ToolsStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/ToolsStep.tsx
index 0c982da6a..501509ff5 100644
--- a/src/components/agents/new-agent-creation/wizard-steps/ToolsStep.tsx
+++ b/src/components/agents/new-agent-creation/wizard-steps/ToolsStep.tsx
@@ -1,60 +1,52 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { type ReactNode } from 'react';
-import type { Tools } from '../../../../Tool.js';
-import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
-import { Byline } from '../../../design-system/Byline.js';
-import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
-import { useWizard } from '../../../wizard/index.js';
-import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
-import { ToolSelector } from '../../ToolSelector.js';
-import type { AgentWizardData } from '../types.js';
+import React, { type ReactNode } from 'react'
+import type { Tools } from '../../../../Tool.js'
+import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
+import { Byline } from '../../../design-system/Byline.js'
+import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
+import { useWizard } from '../../../wizard/index.js'
+import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
+import { ToolSelector } from '../../ToolSelector.js'
+import type { AgentWizardData } from '../types.js'
+
type Props = {
- tools: Tools;
-};
-export function ToolsStep(t0) {
- const $ = _c(9);
- const {
- tools
- } = t0;
- const {
- goNext,
- goBack,
- updateWizardData,
- wizardData
- } = useWizard();
- let t1;
- if ($[0] !== goNext || $[1] !== updateWizardData) {
- t1 = selectedTools => {
- updateWizardData({
- selectedTools
- });
- goNext();
- };
- $[0] = goNext;
- $[1] = updateWizardData;
- $[2] = t1;
- } else {
- t1 = $[2];
- }
- const handleComplete = t1;
- const initialTools = wizardData.selectedTools;
- let t2;
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
- t2 = ;
- $[3] = t2;
- } else {
- t2 = $[3];
- }
- let t3;
- if ($[4] !== goBack || $[5] !== handleComplete || $[6] !== initialTools || $[7] !== tools) {
- t3 = ;
- $[4] = goBack;
- $[5] = handleComplete;
- $[6] = initialTools;
- $[7] = tools;
- $[8] = t3;
- } else {
- t3 = $[8];
- }
- return t3;
+ tools: Tools
+}
+
+export function ToolsStep({ tools }: Props): ReactNode {
+ const { goNext, goBack, updateWizardData, wizardData } =
+ useWizard()
+
+ const handleComplete = (selectedTools: string[] | undefined): void => {
+ updateWizardData({ selectedTools })
+ goNext()
+ }
+
+ // Pass through undefined to preserve "all tools" semantic
+ // ToolSelector will expand it internally for display purposes
+ const initialTools = wizardData.selectedTools
+
+ return (
+
+
+
+
+
+ }
+ >
+
+
+ )
}
diff --git a/src/components/agents/new-agent-creation/wizard-steps/TypeStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/TypeStep.tsx
index 70c085cc5..6ff025492 100644
--- a/src/components/agents/new-agent-creation/wizard-steps/TypeStep.tsx
+++ b/src/components/agents/new-agent-creation/wizard-steps/TypeStep.tsx
@@ -1,102 +1,83 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { type ReactNode, useState } from 'react';
-import { Box, Text } from '../../../../ink.js';
-import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
-import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js';
-import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
-import { Byline } from '../../../design-system/Byline.js';
-import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
-import TextInput from '../../../TextInput.js';
-import { useWizard } from '../../../wizard/index.js';
-import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
-import { validateAgentType } from '../../validateAgent.js';
-import type { AgentWizardData } from '../types.js';
+import React, { type ReactNode, useState } from 'react'
+import { Box, Text } from '../../../../ink.js'
+import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
+import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'
+import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
+import { Byline } from '../../../design-system/Byline.js'
+import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
+import TextInput from '../../../TextInput.js'
+import { useWizard } from '../../../wizard/index.js'
+import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
+import { validateAgentType } from '../../validateAgent.js'
+import type { AgentWizardData } from '../types.js'
+
type Props = {
- existingAgents: AgentDefinition[];
-};
-export function TypeStep(_props) {
- const $ = _c(15);
- const {
- goNext,
- goBack,
- updateWizardData,
- wizardData
- } = useWizard();
- const [agentType, setAgentType] = useState(wizardData.agentType || "");
- const [error, setError] = useState(null);
- const [cursorOffset, setCursorOffset] = useState(agentType.length);
- let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t0 = {
- context: "Settings"
- };
- $[0] = t0;
- } else {
- t0 = $[0];
- }
- useKeybinding("confirm:no", goBack, t0);
- let t1;
- if ($[1] !== goNext || $[2] !== updateWizardData) {
- t1 = value => {
- const trimmedValue = value.trim();
- const validationError = validateAgentType(trimmedValue);
- if (validationError) {
- setError(validationError);
- return;
- }
- setError(null);
- updateWizardData({
- agentType: trimmedValue
- });
- goNext();
- };
- $[1] = goNext;
- $[2] = updateWizardData;
- $[3] = t1;
- } else {
- t1 = $[3];
- }
- const handleSubmit = t1;
- let t2;
- if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
- t2 = ;
- $[4] = t2;
- } else {
- t2 = $[4];
- }
- let t3;
- if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
- t3 = Enter a unique identifier for your agent:;
- $[5] = t3;
- } else {
- t3 = $[5];
- }
- let t4;
- if ($[6] !== agentType || $[7] !== cursorOffset || $[8] !== handleSubmit) {
- t4 = ;
- $[6] = agentType;
- $[7] = cursorOffset;
- $[8] = handleSubmit;
- $[9] = t4;
- } else {
- t4 = $[9];
- }
- let t5;
- if ($[10] !== error) {
- t5 = error && {error};
- $[10] = error;
- $[11] = t5;
- } else {
- t5 = $[11];
- }
- let t6;
- if ($[12] !== t4 || $[13] !== t5) {
- t6 = {t3}{t4}{t5};
- $[12] = t4;
- $[13] = t5;
- $[14] = t6;
- } else {
- t6 = $[14];
- }
- return t6;
+ existingAgents: AgentDefinition[]
+}
+
+export function TypeStep(_props: Props): ReactNode {
+ const { goNext, goBack, updateWizardData, wizardData } =
+ useWizard()
+ const [agentType, setAgentType] = useState(wizardData.agentType || '')
+ const [error, setError] = useState(null)
+ const [cursorOffset, setCursorOffset] = useState(agentType.length)
+
+ // Handle escape key - Go back to MethodStep
+ // Use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
+ useKeybinding('confirm:no', goBack, { context: 'Settings' })
+
+ const handleSubmit = (value: string): void => {
+ const trimmedValue = value.trim()
+ const validationError = validateAgentType(trimmedValue)
+
+ if (validationError) {
+ setError(validationError)
+ return
+ }
+
+ setError(null)
+ updateWizardData({ agentType: trimmedValue })
+ goNext()
+ }
+
+ return (
+
+
+
+
+
+ }
+ >
+
+ Enter a unique identifier for your agent:
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ )
}
diff --git a/src/components/design-system/Byline.tsx b/src/components/design-system/Byline.tsx
index be41b584c..b0ddc97f3 100644
--- a/src/components/design-system/Byline.tsx
+++ b/src/components/design-system/Byline.tsx
@@ -1,10 +1,10 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { Children, isValidElement } from 'react';
-import { Text } from '../../ink.js';
+import React, { Children, isValidElement } from 'react'
+import { Text } from '../../ink.js'
+
type Props = {
/** The items to join with a middot separator */
- children: React.ReactNode;
-};
+ children: React.ReactNode
+}
/**
* Joins children with a middot separator (" · ") for inline metadata display.
@@ -34,43 +34,24 @@ type Props = {
*
*
*/
-export function Byline(t0) {
- const $ = _c(5);
- const {
- children
- } = t0;
- let t1;
- let t2;
- if ($[0] !== children) {
- t2 = Symbol.for("react.early_return_sentinel");
- bb0: {
- const validChildren = Children.toArray(children);
- if (validChildren.length === 0) {
- t2 = null;
- break bb0;
- }
- t1 = validChildren.map(_temp);
- }
- $[0] = children;
- $[1] = t1;
- $[2] = t2;
- } else {
- t1 = $[1];
- t2 = $[2];
+export function Byline({ children }: Props): React.ReactNode {
+ // Children.toArray already filters out null, undefined, and booleans
+ const validChildren = Children.toArray(children)
+
+ if (validChildren.length === 0) {
+ return null
}
- if (t2 !== Symbol.for("react.early_return_sentinel")) {
- return t2;
- }
- let t3;
- if ($[3] !== t1) {
- t3 = <>{t1}>;
- $[3] = t1;
- $[4] = t3;
- } else {
- t3 = $[4];
- }
- return t3;
-}
-function _temp(child, index) {
- return {index > 0 && · }{child};
+
+ return (
+ <>
+ {validChildren.map((child, index) => (
+
+ {index > 0 && · }
+ {child}
+
+ ))}
+ >
+ )
}
diff --git a/src/components/design-system/Dialog.tsx b/src/components/design-system/Dialog.tsx
index 5461c6c74..4472bd0d0 100644
--- a/src/components/design-system/Dialog.tsx
+++ b/src/components/design-system/Dialog.tsx
@@ -1,23 +1,26 @@
-import { c as _c } from "react/compiler-runtime";
-import React from 'react';
-import { type ExitState, useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
-import { Box, Text } from '../../ink.js';
-import { useKeybinding } from '../../keybindings/useKeybinding.js';
-import type { Theme } from '../../utils/theme.js';
-import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
-import { Byline } from './Byline.js';
-import { KeyboardShortcutHint } from './KeyboardShortcutHint.js';
-import { Pane } from './Pane.js';
+import React from 'react'
+import {
+ type ExitState,
+ useExitOnCtrlCDWithKeybindings,
+} from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
+import { Box, Text } from '../../ink.js'
+import { useKeybinding } from '../../keybindings/useKeybinding.js'
+import type { Theme } from '../../utils/theme.js'
+import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
+import { Byline } from './Byline.js'
+import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'
+import { Pane } from './Pane.js'
+
type DialogProps = {
- title: React.ReactNode;
- subtitle?: React.ReactNode;
- children: React.ReactNode;
- onCancel: () => void;
- color?: keyof Theme;
- hideInputGuide?: boolean;
- hideBorder?: boolean;
+ title: React.ReactNode
+ subtitle?: React.ReactNode
+ children: React.ReactNode
+ onCancel: () => void
+ color?: keyof Theme
+ hideInputGuide?: boolean
+ hideBorder?: boolean
/** Custom input guide content. Receives exitState for Ctrl+C/D pending display. */
- inputGuide?: (exitState: ExitState) => React.ReactNode;
+ inputGuide?: (exitState: ExitState) => React.ReactNode
/**
* Controls whether Dialog's built-in confirm:no (Esc/n) and app:exit/interrupt
* (Ctrl-C/D) keybindings are active. Set to `false` while an embedded text
@@ -25,113 +28,73 @@ type DialogProps = {
* consumed by Dialog. TextInput has its own ctrl+c/d handlers (cancel on
* press, delete-forward on ctrl+d with text). Defaults to `true`.
*/
- isCancelActive?: boolean;
-};
-export function Dialog(t0) {
- const $ = _c(27);
- const {
- title,
- subtitle,
- children,
- onCancel,
- color: t1,
- hideInputGuide,
- hideBorder,
- inputGuide,
- isCancelActive: t2
- } = t0;
- const color = t1 === undefined ? "permission" : t1;
- const isCancelActive = t2 === undefined ? true : t2;
- const exitState = useExitOnCtrlCDWithKeybindings(undefined, undefined, isCancelActive);
- let t3;
- if ($[0] !== isCancelActive) {
- t3 = {
- context: "Confirmation",
- isActive: isCancelActive
- };
- $[0] = isCancelActive;
- $[1] = t3;
- } else {
- t3 = $[1];
- }
- useKeybinding("confirm:no", onCancel, t3);
- let t4;
- if ($[2] !== exitState.keyName || $[3] !== exitState.pending) {
- t4 = exitState.pending ? Press {exitState.keyName} again to exit : ;
- $[2] = exitState.keyName;
- $[3] = exitState.pending;
- $[4] = t4;
- } else {
- t4 = $[4];
- }
- const defaultInputGuide = t4;
- let t5;
- if ($[5] !== color || $[6] !== title) {
- t5 = {title};
- $[5] = color;
- $[6] = title;
- $[7] = t5;
- } else {
- t5 = $[7];
- }
- let t6;
- if ($[8] !== subtitle) {
- t6 = subtitle && {subtitle};
- $[8] = subtitle;
- $[9] = t6;
- } else {
- t6 = $[9];
- }
- let t7;
- if ($[10] !== t5 || $[11] !== t6) {
- t7 = {t5}{t6};
- $[10] = t5;
- $[11] = t6;
- $[12] = t7;
- } else {
- t7 = $[12];
- }
- let t8;
- if ($[13] !== children || $[14] !== t7) {
- t8 = {t7}{children};
- $[13] = children;
- $[14] = t7;
- $[15] = t8;
- } else {
- t8 = $[15];
- }
- let t9;
- if ($[16] !== defaultInputGuide || $[17] !== exitState || $[18] !== hideInputGuide || $[19] !== inputGuide) {
- t9 = !hideInputGuide && {inputGuide ? inputGuide(exitState) : defaultInputGuide};
- $[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 = {content};
- $[24] = color;
- $[25] = content;
- $[26] = t11;
- } else {
- t11 = $[26];
- }
- return t11;
+ isCancelActive?: boolean
+}
+
+export function Dialog({
+ title,
+ subtitle,
+ children,
+ onCancel,
+ color = 'permission',
+ hideInputGuide,
+ hideBorder,
+ inputGuide,
+ isCancelActive = true,
+}: DialogProps): React.ReactNode {
+ const exitState = useExitOnCtrlCDWithKeybindings(
+ undefined,
+ undefined,
+ isCancelActive,
+ )
+
+ // Use configurable keybinding for ESC to cancel.
+ // isCancelActive lets consumers (e.g. ElicitationDialog) disable this while
+ // an embedded TextInput is focused, so that keys like 'n' reach the field
+ // instead of being consumed here.
+ useKeybinding('confirm:no', onCancel, {
+ context: 'Confirmation',
+ isActive: isCancelActive,
+ })
+
+ const defaultInputGuide = exitState.pending ? (
+ Press {exitState.keyName} again to exit
+ ) : (
+
+
+
+
+ )
+
+ const content = (
+ <>
+
+
+
+ {title}
+
+ {subtitle && {subtitle}}
+
+ {children}
+
+ {!hideInputGuide && (
+
+
+ {inputGuide ? inputGuide(exitState) : defaultInputGuide}
+
+
+ )}
+ >
+ )
+
+ if (hideBorder) {
+ return content
+ }
+
+ return {content}
}
diff --git a/src/components/design-system/Divider.tsx b/src/components/design-system/Divider.tsx
index 362f4c283..a88982be5 100644
--- a/src/components/design-system/Divider.tsx
+++ b/src/components/design-system/Divider.tsx
@@ -1,33 +1,33 @@
-import { c as _c } from "react/compiler-runtime";
-import React from 'react';
-import { useTerminalSize } from '../../hooks/useTerminalSize.js';
-import { stringWidth } from '../../ink/stringWidth.js';
-import { Ansi, Text } from '../../ink.js';
-import type { Theme } from '../../utils/theme.js';
+import React from 'react'
+import { useTerminalSize } from '../../hooks/useTerminalSize.js'
+import { stringWidth } from '../../ink/stringWidth.js'
+import { Ansi, Text } from '../../ink.js'
+import type { Theme } from '../../utils/theme.js'
+
type DividerProps = {
/**
* Width of the divider in characters.
* Defaults to terminal width.
*/
- width?: number;
+ width?: number
/**
* Theme color for the divider.
* If not provided, dimColor is used.
*/
- color?: keyof Theme;
+ color?: keyof Theme
/**
* Character to use for the divider line.
* @default '─'
*/
- char?: string;
+ char?: string
/**
* Padding to subtract from the width (e.g., for indentation).
* @default 0
*/
- padding?: number;
+ padding?: number
/**
* Title shown in the middle of the divider.
@@ -37,8 +37,8 @@ type DividerProps = {
* // ─────────── Title ───────────
*
*/
- title?: string;
-};
+ title?: string
+}
/**
* A horizontal divider line.
@@ -63,86 +63,35 @@ type DividerProps = {
* // With centered title
*
*/
-export function Divider(t0) {
- const $ = _c(21);
- const {
- width,
- color,
- char: t1,
- padding: t2,
- title
- } = t0;
- const char = t1 === undefined ? "\u2500" : t1;
- const padding = t2 === undefined ? 0 : t2;
- const {
- columns: terminalWidth
- } = useTerminalSize();
- const effectiveWidth = Math.max(0, (width ?? terminalWidth) - padding);
+export function Divider({
+ width,
+ color,
+ char = '─',
+ padding = 0,
+ title,
+}: DividerProps): React.ReactNode {
+ const { columns: terminalWidth } = useTerminalSize()
+ const effectiveWidth = Math.max(0, (width ?? terminalWidth) - padding)
+
if (title) {
- const titleWidth = stringWidth(title) + 2;
- const sideWidth = Math.max(0, effectiveWidth - titleWidth);
- const leftWidth = Math.floor(sideWidth / 2);
- const rightWidth = sideWidth - leftWidth;
- const t3 = !color;
- let t4;
- if ($[0] !== char || $[1] !== leftWidth) {
- t4 = char.repeat(leftWidth);
- $[0] = char;
- $[1] = leftWidth;
- $[2] = t4;
- } else {
- t4 = $[2];
- }
- let t5;
- if ($[3] !== title) {
- t5 = {title};
- $[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 = {t4}{" "}{t5}{" "}{t6};
- $[8] = color;
- $[9] = t3;
- $[10] = t4;
- $[11] = t5;
- $[12] = t6;
- $[13] = t7;
- } else {
- t7 = $[13];
- }
- return t7;
+ const titleWidth = stringWidth(title) + 2 // +2 for spaces around title
+ const sideWidth = Math.max(0, effectiveWidth - titleWidth)
+ const leftWidth = Math.floor(sideWidth / 2)
+ const rightWidth = sideWidth - leftWidth
+ return (
+
+ {char.repeat(leftWidth)}{' '}
+
+ {title}
+ {' '}
+ {char.repeat(rightWidth)}
+
+ )
}
- const t3 = !color;
- let t4;
- if ($[14] !== char || $[15] !== effectiveWidth) {
- t4 = char.repeat(effectiveWidth);
- $[14] = char;
- $[15] = effectiveWidth;
- $[16] = t4;
- } else {
- t4 = $[16];
- }
- let t5;
- if ($[17] !== color || $[18] !== t3 || $[19] !== t4) {
- t5 = {t4};
- $[17] = color;
- $[18] = t3;
- $[19] = t4;
- $[20] = t5;
- } else {
- t5 = $[20];
- }
- return t5;
+
+ return (
+
+ {char.repeat(effectiveWidth)}
+
+ )
}
diff --git a/src/components/design-system/FuzzyPicker.tsx b/src/components/design-system/FuzzyPicker.tsx
index e84f1aafd..fc1b9fe9e 100644
--- a/src/components/design-system/FuzzyPicker.tsx
+++ b/src/components/design-system/FuzzyPicker.tsx
@@ -1,70 +1,73 @@
-import { c as _c } from "react/compiler-runtime";
-import * as React from 'react';
-import { useEffect, useState } from 'react';
-import { useSearchInput } from '../../hooks/useSearchInput.js';
-import { useTerminalSize } from '../../hooks/useTerminalSize.js';
-import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
-import { clamp } from '../../ink/layout/geometry.js';
-import { Box, Text, useTerminalFocus } from '../../ink.js';
-import { SearchBox } from '../SearchBox.js';
-import { Byline } from './Byline.js';
-import { KeyboardShortcutHint } from './KeyboardShortcutHint.js';
-import { ListItem } from './ListItem.js';
-import { Pane } from './Pane.js';
+import * as React from 'react'
+import { useEffect, useState } from 'react'
+import { useSearchInput } from '../../hooks/useSearchInput.js'
+import { useTerminalSize } from '../../hooks/useTerminalSize.js'
+import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
+import { clamp } from '../../ink/layout/geometry.js'
+import { Box, Text, useTerminalFocus } from '../../ink.js'
+import { SearchBox } from '../SearchBox.js'
+import { Byline } from './Byline.js'
+import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'
+import { ListItem } from './ListItem.js'
+import { Pane } from './Pane.js'
+
type PickerAction = {
/** Hint label shown in the byline, e.g. "mention" → "Tab to mention". */
- action: string;
- handler: (item: T) => void;
-};
+ action: string
+ handler: (item: T) => void
+}
+
type Props = {
- title: string;
- placeholder?: string;
- initialQuery?: string;
- items: readonly T[];
- getKey: (item: T) => string;
+ title: string
+ placeholder?: string
+ initialQuery?: string
+ items: readonly T[]
+ getKey: (item: T) => string
/** Keep to one line — preview handles overflow. */
- renderItem: (item: T, isFocused: boolean) => React.ReactNode;
- renderPreview?: (item: T) => React.ReactNode;
+ renderItem: (item: T, isFocused: boolean) => React.ReactNode
+ renderPreview?: (item: T) => React.ReactNode
/** 'right' keeps hints stable (no bounce), but needs width. */
- previewPosition?: 'bottom' | 'right';
- visibleCount?: number;
+ previewPosition?: 'bottom' | 'right'
+ visibleCount?: number
/**
* 'up' puts items[0] at the bottom next to the input (atuin-style). Arrows
* always match screen direction — ↑ walks visually up regardless.
*/
- direction?: 'down' | 'up';
+ direction?: 'down' | 'up'
/** Caller owns filtering: re-filter on each call and pass new items. */
- onQueryChange: (query: string) => void;
+ onQueryChange: (query: string) => void
/** Enter key. Primary action. */
- onSelect: (item: T) => void;
+ onSelect: (item: T) => void
/**
* Tab key. If provided, Tab no longer aliases Enter — it gets its own
* handler and hint. Shift+Tab falls through to this if onShiftTab is unset.
*/
- onTab?: PickerAction;
+ onTab?: PickerAction
/** Shift+Tab key. Gets its own hint. */
- onShiftTab?: PickerAction;
+ onShiftTab?: PickerAction
/**
* Fires when the focused item changes (via arrows or when items reset).
* Useful for async preview loading — keeps I/O out of renderPreview.
*/
- onFocus?: (item: T | undefined) => void;
- onCancel: () => void;
+ onFocus?: (item: T | undefined) => void
+ onCancel: () => void
/** Shown when items is empty. Caller bakes loading/searching state into this. */
- emptyMessage?: string | ((query: string) => string);
+ emptyMessage?: string | ((query: string) => string)
/**
* Status line below the list, e.g. "500+ matches" or "42 matches…".
* Caller decides when to show it — pass undefined to hide.
*/
- matchLabel?: string;
- selectAction?: string;
- extraHints?: React.ReactNode;
-};
-const DEFAULT_VISIBLE = 8;
+ matchLabel?: string
+ selectAction?: string
+ extraHints?: React.ReactNode
+}
+
+const DEFAULT_VISIBLE = 8
// Pane (paddingTop + Divider) + title + 3 gaps + SearchBox (rounded border = 3
// rows) + hints. matchLabel adds +1 when present, accounted for separately.
-const CHROME_ROWS = 10;
-const MIN_VISIBLE = 2;
+const CHROME_ROWS = 10
+const MIN_VISIBLE = 2
+
export function FuzzyPicker({
title,
placeholder = 'Type to search…',
@@ -85,117 +88,168 @@ export function FuzzyPicker({
emptyMessage = 'No results',
matchLabel,
selectAction = 'select',
- extraHints
+ extraHints,
}: Props): React.ReactNode {
- const isTerminalFocused = useTerminalFocus();
- const {
- rows,
- columns
- } = useTerminalSize();
- const [focusedIndex, setFocusedIndex] = useState(0);
+ const isTerminalFocused = useTerminalFocus()
+ const { rows, columns } = useTerminalSize()
+ const [focusedIndex, setFocusedIndex] = useState(0)
// Cap visibleCount so the picker never exceeds the terminal height. When it
// overflows, each re-render (arrow key, ctrl+p) mis-positions the cursor-up
// by the overflow amount and a previously-drawn line flashes blank.
- const visibleCount = Math.max(MIN_VISIBLE, Math.min(requestedVisible, rows - CHROME_ROWS - (matchLabel ? 1 : 0)));
+ const visibleCount = Math.max(
+ MIN_VISIBLE,
+ Math.min(requestedVisible, rows - CHROME_ROWS - (matchLabel ? 1 : 0)),
+ )
// Full hint row with onTab+onShiftTab is ~100 chars and wraps inconsistently
// below that. Compact mode drops shift+tab and shortens labels.
- const compact = columns < 120;
+ const compact = columns < 120
+
const step = (delta: 1 | -1) => {
- setFocusedIndex(i => clamp(i + delta, 0, items.length - 1));
- };
+ setFocusedIndex(i => clamp(i + delta, 0, items.length - 1))
+ }
// onKeyDown fires after useSearchInput's useInput, so onExit must be a
// no-op — return/downArrow are handled by handleKeyDown below. onCancel
// still covers escape/ctrl+c/ctrl+d. Backspace-on-empty is disabled so
// a held backspace doesn't eject the user from the dialog.
- const {
- query,
- cursorOffset
- } = useSearchInput({
+ const { query, cursorOffset } = useSearchInput({
isActive: true,
onExit: () => {},
onCancel,
initialQuery,
- backspaceExitsOnEmpty: false
- });
+ backspaceExitsOnEmpty: false,
+ })
+
const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === 'up' || e.ctrl && e.key === 'p') {
- e.preventDefault();
- e.stopImmediatePropagation();
- step(direction === 'up' ? 1 : -1);
- return;
+ if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ step(direction === 'up' ? 1 : -1)
+ return
}
- if (e.key === 'down' || e.ctrl && e.key === 'n') {
- e.preventDefault();
- e.stopImmediatePropagation();
- step(direction === 'up' ? -1 : 1);
- return;
+ if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ step(direction === 'up' ? -1 : 1)
+ return
}
if (e.key === 'return') {
- e.preventDefault();
- e.stopImmediatePropagation();
- const selected = items[focusedIndex];
- if (selected) onSelect(selected);
- return;
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ const selected = items[focusedIndex]
+ if (selected) onSelect(selected)
+ return
}
if (e.key === 'tab') {
- e.preventDefault();
- e.stopImmediatePropagation();
- const selected = items[focusedIndex];
- if (!selected) return;
- const tabAction = e.shift ? onShiftTab ?? onTab : onTab;
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ const selected = items[focusedIndex]
+ if (!selected) return
+ const tabAction = e.shift ? (onShiftTab ?? onTab) : onTab
if (tabAction) {
- tabAction.handler(selected);
+ tabAction.handler(selected)
} else {
- onSelect(selected);
+ onSelect(selected)
}
}
- };
+ }
+
useEffect(() => {
- onQueryChange(query);
- setFocusedIndex(0);
+ onQueryChange(query)
+ setFocusedIndex(0)
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [query]);
+ }, [query])
+
useEffect(() => {
- setFocusedIndex(i => clamp(i, 0, items.length - 1));
- }, [items.length]);
- const focused = items[focusedIndex];
+ setFocusedIndex(i => clamp(i, 0, items.length - 1))
+ }, [items.length])
+
+ const focused = items[focusedIndex]
useEffect(() => {
- onFocus?.(focused);
+ onFocus?.(focused)
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [focused]);
- const windowStart = clamp(focusedIndex - visibleCount + 1, 0, items.length - visibleCount);
- const visible = items.slice(windowStart, windowStart + visibleCount);
- const emptyText = typeof emptyMessage === 'function' ? emptyMessage(query) : emptyMessage;
- const searchBox = ;
- const listBlock =
;
- const preview = renderPreview && focused ?
+ }, [focused])
+
+ const windowStart = clamp(
+ focusedIndex - visibleCount + 1,
+ 0,
+ items.length - visibleCount,
+ )
+ const visible = items.slice(windowStart, windowStart + visibleCount)
+
+ const emptyText =
+ typeof emptyMessage === 'function' ? emptyMessage(query) : emptyMessage
+
+ const searchBox = (
+
+ )
+
+ const listBlock = (
+
+ )
+
+ const preview =
+ renderPreview && focused ? (
+
{renderPreview(focused)}
- : null;
+
+ ) : null
// Structure must not depend on preview truthiness — when focused goes
// undefined (e.g. delete clears matches), switching row→fragment would
// change both layout AND gap count, bouncing the searchBox below.
- const listGroup = renderPreview && previewPosition === 'right' ?
+ const listGroup =
+ renderPreview && previewPosition === 'right' ? (
+
{listBlock}
{matchLabel && {matchLabel}}
{preview ?? }
- :
- // Box (not fragment) so the outer gap={1} doesn't insert a blank line
- // between list/matchLabel/preview — that read as extra space above the
- // prompt in direction='up'.
-
+
+ ) : (
+ // Box (not fragment) so the outer gap={1} doesn't insert a blank line
+ // between list/matchLabel/preview — that read as extra space above the
+ // prompt in direction='up'.
+
{listBlock}
{matchLabel && {matchLabel}}
{preview}
- ;
- const inputAbove = direction !== 'up';
- return
-
+
+ )
+
+ const inputAbove = direction !== 'up'
+ return (
+
+
{title}
@@ -204,108 +258,93 @@ export function FuzzyPicker({
{!inputAbove && searchBox}
-
-
- {onTab && }
- {onShiftTab && !compact && }
+
+
+ {onTab && (
+
+ )}
+ {onShiftTab && !compact && (
+
+ )}
{extraHints}
- ;
+
+ )
}
-type ListProps = Pick, 'visibleCount' | 'direction' | 'getKey' | 'renderItem'> & {
- visible: readonly T[];
- windowStart: number;
- total: number;
- focusedIndex: number;
- emptyText: string;
-};
-function List(t0) {
- const $ = _c(27);
- const {
- visible,
- windowStart,
- visibleCount,
- total,
- focusedIndex,
- direction,
- getKey,
- renderItem,
- emptyText
- } = t0;
+
+type ListProps = Pick<
+ Props,
+ 'visibleCount' | 'direction' | 'getKey' | 'renderItem'
+> & {
+ visible: readonly T[]
+ windowStart: number
+ total: number
+ focusedIndex: number
+ emptyText: string
+}
+
+function List({
+ visible,
+ windowStart,
+ visibleCount,
+ total,
+ focusedIndex,
+ direction,
+ getKey,
+ renderItem,
+ emptyText,
+}: ListProps): React.ReactNode {
if (visible.length === 0) {
- let t1;
- if ($[0] !== emptyText) {
- t1 = {emptyText};
- $[0] = emptyText;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- let t2;
- if ($[2] !== t1 || $[3] !== visibleCount) {
- t2 = {t1};
- $[2] = t1;
- $[3] = visibleCount;
- $[4] = t2;
- } else {
- t2 = $[4];
- }
- return t2;
+ return (
+
+ {emptyText}
+
+ )
}
- let t1;
- if ($[5] !== direction || $[6] !== focusedIndex || $[7] !== getKey || $[8] !== renderItem || $[9] !== total || $[10] !== visible || $[11] !== visibleCount || $[12] !== windowStart) {
- let t2;
- if ($[14] !== direction || $[15] !== focusedIndex || $[16] !== getKey || $[17] !== renderItem || $[18] !== total || $[19] !== visible.length || $[20] !== visibleCount || $[21] !== windowStart) {
- t2 = (item, i) => {
- const actualIndex = windowStart + i;
- const isFocused = actualIndex === focusedIndex;
- const atLowEdge = i === 0 && windowStart > 0;
- const atHighEdge = i === visible.length - 1 && windowStart + visibleCount < total;
- return {renderItem(item, isFocused)};
- };
- $[14] = direction;
- $[15] = focusedIndex;
- $[16] = getKey;
- $[17] = renderItem;
- $[18] = total;
- $[19] = visible.length;
- $[20] = visibleCount;
- $[21] = windowStart;
- $[22] = t2;
- } else {
- t2 = $[22];
- }
- t1 = visible.map(t2);
- $[5] = direction;
- $[6] = focusedIndex;
- $[7] = getKey;
- $[8] = renderItem;
- $[9] = total;
- $[10] = visible;
- $[11] = visibleCount;
- $[12] = windowStart;
- $[13] = t1;
- } else {
- t1 = $[13];
- }
- const rows = t1;
- const t2 = direction === "up" ? "column-reverse" : "column";
- let t3;
- if ($[23] !== rows || $[24] !== t2 || $[25] !== visibleCount) {
- t3 = {rows};
- $[23] = rows;
- $[24] = t2;
- $[25] = visibleCount;
- $[26] = t3;
- } else {
- t3 = $[26];
- }
- return t3;
+
+ const rows = visible.map((item, i) => {
+ const actualIndex = windowStart + i
+ const isFocused = actualIndex === focusedIndex
+ const atLowEdge = i === 0 && windowStart > 0
+ const atHighEdge =
+ i === visible.length - 1 && windowStart + visibleCount! < total
+ return (
+
+ {renderItem(item, isFocused)}
+
+ )
+ })
+
+ return (
+
+ {rows}
+
+ )
}
+
function firstWord(s: string): string {
- const i = s.indexOf(' ');
- return i === -1 ? s : s.slice(0, i);
+ const i = s.indexOf(' ')
+ return i === -1 ? s : s.slice(0, i)
}
diff --git a/src/components/design-system/KeyboardShortcutHint.tsx b/src/components/design-system/KeyboardShortcutHint.tsx
index 19b51d05b..7d3c136d1 100644
--- a/src/components/design-system/KeyboardShortcutHint.tsx
+++ b/src/components/design-system/KeyboardShortcutHint.tsx
@@ -1,16 +1,16 @@
-import { c as _c } from "react/compiler-runtime";
-import React from 'react';
-import Text from '../../ink/components/Text.js';
+import React from 'react'
+import Text from '../../ink/components/Text.js'
+
type Props = {
/** The key or chord to display (e.g., "ctrl+o", "Enter", "↑/↓") */
- shortcut: string;
+ shortcut: string
/** The action the key performs (e.g., "expand", "select", "navigate") */
- action: string;
+ action: string
/** Whether to wrap the hint in parentheses. Default: false */
- parens?: boolean;
+ parens?: boolean
/** Whether to render the shortcut in bold. Default: false */
- bold?: boolean;
-};
+ bold?: boolean
+}
/**
* Renders a keyboard shortcut hint like "ctrl+o to expand" or "(tab to toggle)"
@@ -35,46 +35,24 @@ type Props = {
*
*
*/
-export function KeyboardShortcutHint(t0) {
- const $ = _c(9);
- const {
- shortcut,
- action,
- parens: t1,
- bold: t2
- } = t0;
- const parens = t1 === undefined ? false : t1;
- const bold = t2 === undefined ? false : t2;
- let t3;
- if ($[0] !== bold || $[1] !== shortcut) {
- t3 = bold ? {shortcut} : shortcut;
- $[0] = bold;
- $[1] = shortcut;
- $[2] = t3;
- } else {
- t3 = $[2];
- }
- const shortcutText = t3;
+export function KeyboardShortcutHint({
+ shortcut,
+ action,
+ parens = false,
+ bold = false,
+}: Props): React.ReactNode {
+ const shortcutText = bold ? {shortcut} : shortcut
+
if (parens) {
- let t4;
- if ($[3] !== action || $[4] !== shortcutText) {
- t4 = ({shortcutText} to {action});
- $[3] = action;
- $[4] = shortcutText;
- $[5] = t4;
- } else {
- t4 = $[5];
- }
- return t4;
+ return (
+
+ ({shortcutText} to {action})
+
+ )
}
- let t4;
- if ($[6] !== action || $[7] !== shortcutText) {
- t4 = {shortcutText} to {action};
- $[6] = action;
- $[7] = shortcutText;
- $[8] = t4;
- } else {
- t4 = $[8];
- }
- return t4;
+ return (
+
+ {shortcutText} to {action}
+
+ )
}
diff --git a/src/components/design-system/ListItem.tsx b/src/components/design-system/ListItem.tsx
index 0ee8068cc..2d142be03 100644
--- a/src/components/design-system/ListItem.tsx
+++ b/src/components/design-system/ListItem.tsx
@@ -1,44 +1,44 @@
-import { c as _c } from "react/compiler-runtime";
-import figures from 'figures';
-import type { ReactNode } from 'react';
-import React from 'react';
-import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js';
-import { Box, Text } from '../../ink.js';
+import figures from 'figures'
+import type { ReactNode } from 'react'
+import React from 'react'
+import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js'
+import { Box, Text } from '../../ink.js'
+
type ListItemProps = {
/**
* Whether this item is currently focused (keyboard selection).
* Shows the pointer indicator (❯) when true.
*/
- isFocused: boolean;
+ isFocused: boolean
/**
* Whether this item is selected (chosen/checked).
* Shows the checkmark indicator (✓) when true.
* @default false
*/
- isSelected?: boolean;
+ isSelected?: boolean
/**
* The content to display for this item.
*/
- children: ReactNode;
+ children: ReactNode
/**
* Optional description text displayed below the main content.
*/
- description?: string;
+ description?: string
/**
* Show a down arrow indicator instead of pointer (for scroll hints).
* Only applies when not focused.
*/
- showScrollDown?: boolean;
+ showScrollDown?: boolean
/**
* Show an up arrow indicator instead of pointer (for scroll hints).
* Only applies when not focused.
*/
- showScrollUp?: boolean;
+ showScrollUp?: boolean
/**
* Whether to apply automatic styling to the children based on focus/selection state.
@@ -46,21 +46,21 @@ type ListItemProps = {
* - When false: children are rendered as-is, allowing custom styling
* @default true
*/
- styled?: boolean;
+ styled?: boolean
/**
* Whether this item is disabled. Disabled items show dimmed text and no indicators.
* @default false
*/
- disabled?: boolean;
+ disabled?: boolean
/**
* Whether this ListItem should declare the terminal cursor position.
* Set false when a child (e.g. BaseTextInput) declares its own cursor.
* @default true
*/
- declareCursor?: boolean;
-};
+ declareCursor?: boolean
+}
/**
* A list item component for selection UIs (dropdowns, multi-selects, menus).
@@ -101,143 +101,88 @@ type ListItemProps = {
* Custom styled content
*
*/
-export function ListItem(t0) {
- const $ = _c(32);
- const {
- isFocused,
- isSelected: t1,
- children,
- description,
- showScrollDown,
- showScrollUp,
- styled: t2,
- disabled: t3,
- declareCursor
- } = t0;
- const isSelected = t1 === undefined ? false : t1;
- const styled = t2 === undefined ? true : t2;
- const disabled = t3 === undefined ? false : t3;
- let t4;
- if ($[0] !== disabled || $[1] !== isFocused || $[2] !== showScrollDown || $[3] !== showScrollUp) {
- t4 = function renderIndicator() {
- if (disabled) {
- return ;
- }
- if (isFocused) {
- return {figures.pointer};
- }
- if (showScrollDown) {
- return {figures.arrowDown};
- }
- if (showScrollUp) {
- return {figures.arrowUp};
- }
- return ;
- };
- $[0] = disabled;
- $[1] = isFocused;
- $[2] = showScrollDown;
- $[3] = showScrollUp;
- $[4] = t4;
- } else {
- t4 = $[4];
+export function ListItem({
+ isFocused,
+ isSelected = false,
+ children,
+ description,
+ showScrollDown,
+ showScrollUp,
+ styled = true,
+ disabled = false,
+ declareCursor,
+}: ListItemProps): React.ReactNode {
+ // Determine which indicator to show
+ function renderIndicator(): ReactNode {
+ if (disabled) {
+ return
+ }
+
+ if (isFocused) {
+ return {figures.pointer}
+ }
+
+ if (showScrollDown) {
+ return {figures.arrowDown}
+ }
+
+ if (showScrollUp) {
+ return {figures.arrowUp}
+ }
+
+ return
}
- const renderIndicator = t4;
- let t5;
- if ($[5] !== disabled || $[6] !== isFocused || $[7] !== isSelected || $[8] !== styled) {
- const getTextColor = function getTextColor() {
- if (disabled) {
- return "inactive";
- }
- if (!styled) {
- return;
- }
- if (isSelected) {
- return "success";
- }
- if (isFocused) {
- return "suggestion";
- }
- };
- t5 = getTextColor();
- $[5] = disabled;
- $[6] = isFocused;
- $[7] = isSelected;
- $[8] = styled;
- $[9] = t5;
- } else {
- t5 = $[9];
+
+ // Determine text color based on state
+ function getTextColor(): 'success' | 'suggestion' | 'inactive' | undefined {
+ if (disabled) {
+ return 'inactive'
+ }
+
+ if (!styled) {
+ return undefined
+ }
+
+ if (isSelected) {
+ return 'success'
+ }
+
+ if (isFocused) {
+ return 'suggestion'
+ }
+
+ return undefined
}
- const textColor = t5;
- const t6 = isFocused && !disabled && declareCursor !== false;
- let t7;
- if ($[10] !== t6) {
- t7 = {
- line: 0,
- column: 0,
- active: t6
- };
- $[10] = t6;
- $[11] = t7;
- } else {
- t7 = $[11];
- }
- const cursorRef = useDeclaredCursor(t7);
- let t8;
- if ($[12] !== renderIndicator) {
- t8 = renderIndicator();
- $[12] = renderIndicator;
- $[13] = t8;
- } else {
- t8 = $[13];
- }
- let t9;
- if ($[14] !== children || $[15] !== disabled || $[16] !== styled || $[17] !== textColor) {
- t9 = styled ? {children} : children;
- $[14] = children;
- $[15] = disabled;
- $[16] = styled;
- $[17] = textColor;
- $[18] = t9;
- } else {
- t9 = $[18];
- }
- let t10;
- if ($[19] !== disabled || $[20] !== isSelected) {
- t10 = isSelected && !disabled && {figures.tick};
- $[19] = disabled;
- $[20] = isSelected;
- $[21] = t10;
- } else {
- t10 = $[21];
- }
- let t11;
- if ($[22] !== t10 || $[23] !== t8 || $[24] !== t9) {
- t11 = {t8}{t9}{t10};
- $[22] = t10;
- $[23] = t8;
- $[24] = t9;
- $[25] = t11;
- } else {
- t11 = $[25];
- }
- let t12;
- if ($[26] !== description) {
- t12 = description && {description};
- $[26] = description;
- $[27] = t12;
- } else {
- t12 = $[27];
- }
- let t13;
- if ($[28] !== cursorRef || $[29] !== t11 || $[30] !== t12) {
- t13 = {t11}{t12};
- $[28] = cursorRef;
- $[29] = t11;
- $[30] = t12;
- $[31] = t13;
- } else {
- t13 = $[31];
- }
- return t13;
+
+ const textColor = getTextColor()
+
+ // Park the native terminal cursor on the pointer indicator so screen
+ // readers / magnifiers track the focused item. (0,0) is the top-left of
+ // this Box, where the pointer renders.
+ const cursorRef = useDeclaredCursor({
+ line: 0,
+ column: 0,
+ active: isFocused && !disabled && declareCursor !== false,
+ })
+
+ return (
+
+
+ {renderIndicator()}
+ {styled ? (
+
+ {children}
+
+ ) : (
+ children
+ )}
+ {isSelected && !disabled && {figures.tick}}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ )
}
diff --git a/src/components/design-system/LoadingState.tsx b/src/components/design-system/LoadingState.tsx
index aa05dd941..046f726fa 100644
--- a/src/components/design-system/LoadingState.tsx
+++ b/src/components/design-system/LoadingState.tsx
@@ -1,30 +1,30 @@
-import { c as _c } from "react/compiler-runtime";
-import React from 'react';
-import { Box, Text } from '../../ink.js';
-import { Spinner } from '../Spinner.js';
+import React from 'react'
+import { Box, Text } from '../../ink.js'
+import { Spinner } from '../Spinner.js'
+
type LoadingStateProps = {
/**
* The loading message to display next to the spinner.
*/
- message: string;
+ message: string
/**
* Display the message in bold.
* @default false
*/
- bold?: boolean;
+ bold?: boolean
/**
* Display the message in dimmed color.
* @default false
*/
- dimColor?: boolean;
+ dimColor?: boolean
/**
* Optional subtitle displayed below the main message.
*/
- subtitle?: string;
-};
+ subtitle?: string
+}
/**
* A spinner with loading message for async operations.
@@ -45,49 +45,22 @@ type LoadingStateProps = {
* subtitle="Fetching your Claude Code sessions..."
* />
*/
-export function LoadingState(t0) {
- const $ = _c(10);
- const {
- message,
- bold: t1,
- dimColor: t2,
- subtitle
- } = t0;
- const bold = t1 === undefined ? false : t1;
- const dimColor = t2 === undefined ? false : t2;
- let t3;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t3 = ;
- $[0] = t3;
- } else {
- t3 = $[0];
- }
- let t4;
- if ($[1] !== bold || $[2] !== dimColor || $[3] !== message) {
- t4 = {t3}{" "}{message};
- $[1] = bold;
- $[2] = dimColor;
- $[3] = message;
- $[4] = t4;
- } else {
- t4 = $[4];
- }
- let t5;
- if ($[5] !== subtitle) {
- t5 = subtitle && {subtitle};
- $[5] = subtitle;
- $[6] = t5;
- } else {
- t5 = $[6];
- }
- let t6;
- if ($[7] !== t4 || $[8] !== t5) {
- t6 = {t4}{t5};
- $[7] = t4;
- $[8] = t5;
- $[9] = t6;
- } else {
- t6 = $[9];
- }
- return t6;
+export function LoadingState({
+ message,
+ bold = false,
+ dimColor = false,
+ subtitle,
+}: LoadingStateProps): React.ReactNode {
+ return (
+
+
+
+
+ {' '}
+ {message}
+
+
+ {subtitle && {subtitle}}
+
+ )
}
diff --git a/src/components/design-system/Pane.tsx b/src/components/design-system/Pane.tsx
index 4f1264bea..9c10907d3 100644
--- a/src/components/design-system/Pane.tsx
+++ b/src/components/design-system/Pane.tsx
@@ -1,16 +1,16 @@
-import { c as _c } from "react/compiler-runtime";
-import React from 'react';
-import { useIsInsideModal } from '../../context/modalContext.js';
-import { Box } from '../../ink.js';
-import type { Theme } from '../../utils/theme.js';
-import { Divider } from './Divider.js';
+import React from 'react'
+import { useIsInsideModal } from '../../context/modalContext.js'
+import { Box } from '../../ink.js'
+import type { Theme } from '../../utils/theme.js'
+import { Divider } from './Divider.js'
+
type PaneProps = {
- children: React.ReactNode;
+ children: React.ReactNode
/**
* Theme color for the top border line.
*/
- color?: keyof Theme;
-};
+ color?: keyof Theme
+}
/**
* A pane — a region of the terminal that appears below the REPL prompt,
@@ -30,47 +30,28 @@ type PaneProps = {
* ...
*
*/
-export function Pane(t0) {
- const $ = _c(9);
- const {
- children,
- color
- } = t0;
+export function Pane({ children, color }: PaneProps): React.ReactNode {
+ // When rendered inside FullscreenLayout's modal slot, its ▔ divider IS
+ // the frame. Skip our own Divider (would double-frame) and the extra top
+ // padding. This lets slash-command screens that wrap in Pane (e.g.
+ // /model → ModelPicker) route through the modal slot unchanged.
if (useIsInsideModal()) {
- let t1;
- if ($[0] !== children) {
- t1 = {children};
- $[0] = children;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- return t1;
+ // flexShrink=0: the modal slot's absolute Box has no explicit height
+ // (grows to fit, maxHeight cap). With flexGrow=1, re-renders cause
+ // yoga to resolve this Box's height to 0 against the undetermined
+ // parent — /permissions body blanks on Down arrow. See #23592.
+ return (
+
+ {children}
+
+ )
}
- let t1;
- if ($[2] !== color) {
- t1 = ;
- $[2] = color;
- $[3] = t1;
- } else {
- t1 = $[3];
- }
- let t2;
- if ($[4] !== children) {
- t2 = {children};
- $[4] = children;
- $[5] = t2;
- } else {
- t2 = $[5];
- }
- let t3;
- if ($[6] !== t1 || $[7] !== t2) {
- t3 = {t1}{t2};
- $[6] = t1;
- $[7] = t2;
- $[8] = t3;
- } else {
- t3 = $[8];
- }
- return t3;
+ return (
+
+
+
+ {children}
+
+
+ )
}
diff --git a/src/components/design-system/ProgressBar.tsx b/src/components/design-system/ProgressBar.tsx
index 0d27c514b..590fcd265 100644
--- a/src/components/design-system/ProgressBar.tsx
+++ b/src/components/design-system/ProgressBar.tsx
@@ -1,85 +1,54 @@
-import { c as _c } from "react/compiler-runtime";
-import React from 'react';
-import { Text } from '../../ink.js';
-import type { Theme } from '../../utils/theme.js';
+import React from 'react'
+import { Text } from '../../ink.js'
+import type { Theme } from '../../utils/theme.js'
+
type Props = {
/**
* How much progress to display, between 0 and 1 inclusive
*/
- ratio: number; // [0, 1]
+ ratio: number // [0, 1]
/**
* How many characters wide to draw the progress bar
*/
- width: number; // how many characters wide
+ width: number // how many characters wide
/**
* Optional color for the filled portion of the bar
*/
- fillColor?: keyof Theme;
+ fillColor?: keyof Theme
/**
* Optional color for the empty portion of the bar
*/
- emptyColor?: keyof Theme;
-};
-const BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
-export function ProgressBar(t0) {
- const $ = _c(13);
- const {
- ratio: inputRatio,
- width,
- fillColor,
- emptyColor
- } = t0;
- const ratio = Math.min(1, Math.max(0, inputRatio));
- const whole = Math.floor(ratio * width);
- let t1;
- if ($[0] !== whole) {
- t1 = BLOCKS[BLOCKS.length - 1].repeat(whole);
- $[0] = whole;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- let segments;
- if ($[2] !== ratio || $[3] !== t1 || $[4] !== whole || $[5] !== width) {
- segments = [t1];
- if (whole < width) {
- const remainder = ratio * width - whole;
- const middle = Math.floor(remainder * BLOCKS.length);
- segments.push(BLOCKS[middle]);
- const empty = width - whole - 1;
- if (empty > 0) {
- let t2;
- if ($[7] !== empty) {
- t2 = BLOCKS[0].repeat(empty);
- $[7] = empty;
- $[8] = t2;
- } else {
- t2 = $[8];
- }
- segments.push(t2);
- }
- }
- $[2] = ratio;
- $[3] = t1;
- $[4] = whole;
- $[5] = width;
- $[6] = segments;
- } else {
- segments = $[6];
- }
- const t2 = segments.join("");
- let t3;
- if ($[9] !== emptyColor || $[10] !== fillColor || $[11] !== t2) {
- t3 = {t2};
- $[9] = emptyColor;
- $[10] = fillColor;
- $[11] = t2;
- $[12] = t3;
- } else {
- t3 = $[12];
- }
- return t3;
+ emptyColor?: keyof Theme
+}
+
+const BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']
+
+export function ProgressBar({
+ ratio: inputRatio,
+ width,
+ fillColor,
+ emptyColor,
+}: Props): React.ReactNode {
+ const ratio = Math.min(1, Math.max(0, inputRatio))
+ const whole = Math.floor(ratio * width)
+ const segments = [BLOCKS[BLOCKS.length - 1]!.repeat(whole)]
+ if (whole < width) {
+ const remainder = ratio * width - whole
+ const middle = Math.floor(remainder * BLOCKS.length)
+ segments.push(BLOCKS[middle]!)
+
+ const empty = width - whole - 1
+ if (empty > 0) {
+ segments.push(BLOCKS[0]!.repeat(empty))
+ }
+ }
+
+ return (
+
+ {segments.join('')}
+
+ )
}
diff --git a/src/components/design-system/Ratchet.tsx b/src/components/design-system/Ratchet.tsx
index a63cffb33..91580ff05 100644
--- a/src/components/design-system/Ratchet.tsx
+++ b/src/components/design-system/Ratchet.tsx
@@ -1,79 +1,45 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
-import { useTerminalSize } from '../../hooks/useTerminalSize.js';
-import { useTerminalViewport } from '../../ink/hooks/use-terminal-viewport.js';
-import { Box, type DOMElement, measureElement } from '../../ink.js';
+import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'
+import { useTerminalSize } from '../../hooks/useTerminalSize.js'
+import { useTerminalViewport } from '../../ink/hooks/use-terminal-viewport.js'
+import { Box, type DOMElement, measureElement } from '../../ink.js'
+
type Props = {
- children: React.ReactNode;
- lock?: 'always' | 'offscreen';
-};
-export function Ratchet(t0) {
- const $ = _c(10);
- const {
- children,
- lock: t1
- } = t0;
- const lock = t1 === undefined ? "always" : t1;
- const [viewportRef, t2] = useTerminalViewport();
- const {
- isVisible
- } = t2;
- const {
- rows
- } = useTerminalSize();
- const innerRef = useRef(null);
- const maxHeight = useRef(0);
- const [minHeight, setMinHeight] = useState(0);
- let t3;
- if ($[0] !== viewportRef) {
- t3 = el => {
- viewportRef(el);
- };
- $[0] = viewportRef;
- $[1] = t3;
- } else {
- t3 = $[1];
- }
- const outerRef = t3;
- const engaged = lock === "always" || !isVisible;
- let t4;
- if ($[2] !== rows) {
- t4 = () => {
- if (!innerRef.current) {
- return;
- }
- const {
- height
- } = measureElement(innerRef.current);
- if (height > maxHeight.current) {
- maxHeight.current = Math.min(height, rows);
- setMinHeight(maxHeight.current);
- }
- };
- $[2] = rows;
- $[3] = t4;
- } else {
- t4 = $[3];
- }
- useLayoutEffect(t4);
- const t5 = engaged ? minHeight : undefined;
- let t6;
- if ($[4] !== children) {
- t6 = {children};
- $[4] = children;
- $[5] = t6;
- } else {
- t6 = $[5];
- }
- let t7;
- if ($[6] !== outerRef || $[7] !== t5 || $[8] !== t6) {
- t7 = {t6};
- $[6] = outerRef;
- $[7] = t5;
- $[8] = t6;
- $[9] = t7;
- } else {
- t7 = $[9];
- }
- return t7;
+ children: React.ReactNode
+ lock?: 'always' | 'offscreen'
+}
+
+export function Ratchet({ children, lock = 'always' }: Props): React.ReactNode {
+ const [viewportRef, { isVisible }] = useTerminalViewport()
+ const { rows } = useTerminalSize()
+ const innerRef = useRef(null)
+ const maxHeight = useRef(0)
+ const [minHeight, setMinHeight] = useState(0)
+
+ const outerRef = useCallback(
+ (el: DOMElement | null) => {
+ viewportRef(el)
+ },
+ [viewportRef],
+ )
+
+ const engaged = lock === 'always' || !isVisible
+
+ useLayoutEffect(() => {
+ if (!innerRef.current) {
+ return
+ }
+ const { height } = measureElement(innerRef.current)
+ if (height > maxHeight.current) {
+ maxHeight.current = Math.min(height, rows)
+ setMinHeight(maxHeight.current)
+ }
+ })
+
+ return (
+
+
+ {children}
+
+
+ )
}
diff --git a/src/components/design-system/StatusIcon.tsx b/src/components/design-system/StatusIcon.tsx
index d50693edd..832c83a9e 100644
--- a/src/components/design-system/StatusIcon.tsx
+++ b/src/components/design-system/StatusIcon.tsx
@@ -1,8 +1,9 @@
-import { c as _c } from "react/compiler-runtime";
-import figures from 'figures';
-import React from 'react';
-import { Text } from '../../ink.js';
-type Status = 'success' | 'error' | 'warning' | 'info' | 'pending' | 'loading';
+import figures from 'figures'
+import React from 'react'
+import { Text } from '../../ink.js'
+
+type Status = 'success' | 'error' | 'warning' | 'info' | 'pending' | 'loading'
+
type Props = {
/**
* The status to display. Determines both the icon and color.
@@ -14,42 +15,28 @@ type Props = {
* - `pending`: Dimmed circle (○)
* - `loading`: Dimmed ellipsis (…)
*/
- status: Status;
+ status: Status
/**
* Include a trailing space after the icon. Useful when followed by text.
* @default false
*/
- withSpace?: boolean;
-};
-const STATUS_CONFIG: Record = {
- success: {
- icon: figures.tick,
- color: 'success'
- },
- error: {
- icon: figures.cross,
- color: 'error'
- },
- warning: {
- icon: figures.warning,
- color: 'warning'
- },
- info: {
- icon: figures.info,
- color: 'suggestion'
- },
- pending: {
- icon: figures.circle,
- color: undefined
- },
- loading: {
- icon: '…',
- color: undefined
+ withSpace?: boolean
+}
+
+const STATUS_CONFIG: Record<
+ Status,
+ {
+ icon: string
+ color: 'success' | 'error' | 'warning' | 'suggestion' | undefined
}
-};
+> = {
+ success: { icon: figures.tick, color: 'success' },
+ error: { icon: figures.cross, color: 'error' },
+ warning: { icon: figures.warning, color: 'warning' },
+ info: { icon: figures.info, color: 'suggestion' },
+ pending: { icon: figures.circle, color: undefined },
+ loading: { icon: '…', color: undefined },
+}
/**
* Renders a status indicator icon with appropriate color.
@@ -69,26 +56,16 @@ const STATUS_CONFIG: Record
*/
-export function StatusIcon(t0) {
- const $ = _c(5);
- const {
- status,
- withSpace: t1
- } = t0;
- const withSpace = t1 === undefined ? false : t1;
- const config = STATUS_CONFIG[status];
- const t2 = !config.color;
- const t3 = withSpace && " ";
- let t4;
- if ($[0] !== config.color || $[1] !== config.icon || $[2] !== t2 || $[3] !== t3) {
- t4 = {config.icon}{t3};
- $[0] = config.color;
- $[1] = config.icon;
- $[2] = t2;
- $[3] = t3;
- $[4] = t4;
- } else {
- t4 = $[4];
- }
- return t4;
+export function StatusIcon({
+ status,
+ withSpace = false,
+}: Props): React.ReactNode {
+ const config = STATUS_CONFIG[status]
+
+ return (
+
+ {config.icon}
+ {withSpace && ' '}
+
+ )
}
diff --git a/src/components/design-system/Tabs.tsx b/src/components/design-system/Tabs.tsx
index db8d0d59e..40bae7baa 100644
--- a/src/components/design-system/Tabs.tsx
+++ b/src/components/design-system/Tabs.tsx
@@ -1,28 +1,37 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
-import { useIsInsideModal, useModalScrollRef } from '../../context/modalContext.js';
-import { useTerminalSize } from '../../hooks/useTerminalSize.js';
-import ScrollBox from '../../ink/components/ScrollBox.js';
-import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
-import { stringWidth } from '../../ink/stringWidth.js';
-import { Box, Text } from '../../ink.js';
-import { useKeybindings } from '../../keybindings/useKeybinding.js';
-import type { Theme } from '../../utils/theme.js';
+import React, {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useState,
+} from 'react'
+import {
+ useIsInsideModal,
+ useModalScrollRef,
+} from '../../context/modalContext.js'
+import { useTerminalSize } from '../../hooks/useTerminalSize.js'
+import ScrollBox from '../../ink/components/ScrollBox.js'
+import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
+import { stringWidth } from '../../ink/stringWidth.js'
+import { Box, Text } from '../../ink.js'
+import { useKeybindings } from '../../keybindings/useKeybinding.js'
+import type { Theme } from '../../utils/theme.js'
+
type TabsProps = {
- children: Array>;
- title?: string;
- color?: keyof Theme;
- defaultTab?: string;
- hidden?: boolean;
- useFullWidth?: boolean;
+ children: Array>
+ title?: string
+ color?: keyof Theme
+ defaultTab?: string
+ hidden?: boolean
+ useFullWidth?: boolean
/** Controlled mode: current selected tab id/title */
- selectedTab?: string;
+ selectedTab?: string
/** Controlled mode: callback when tab changes */
- onTabChange?: (tabId: string) => void;
+ onTabChange?: (tabId: string) => void
/** Optional banner to display below tabs header */
- banner?: React.ReactNode;
+ banner?: React.ReactNode
/** Disable keyboard navigation (e.g. when a child component handles arrow keys) */
- disableNavigation?: boolean;
+ disableNavigation?: boolean
/**
* Initial focus state for the tab header row. Defaults to true (header
* focused, nav always works). Keep the default for Select/list content —
@@ -31,28 +40,30 @@ type TabsProps = {
* content actually binds left/right/tab (e.g. enum cycling), and show a
* "↑ tabs" footer hint — without it tabs look broken.
*/
- initialHeaderFocused?: boolean;
+ initialHeaderFocused?: boolean
/**
* Fixed height for the content area. When set, all tabs render within the
* same height (overflow hidden) so switching tabs doesn't cause layout
* shifts. Shorter tabs get whitespace; taller tabs are clipped.
*/
- contentHeight?: number;
+ contentHeight?: number
/**
* Let Tab/←/→ switch tabs from focused content. Opt-in since some
* content uses those keys; pass a reactive boolean to cede them when
* needed. Switching from content focuses the header.
*/
- navFromContent?: boolean;
-};
+ navFromContent?: boolean
+}
+
type TabsContextValue = {
- selectedTab: string | undefined;
- width: number | undefined;
- headerFocused: boolean;
- focusHeader: () => void;
- blurHeader: () => void;
- registerOptIn: () => () => void;
-};
+ selectedTab: string | undefined
+ width: number | undefined
+ headerFocused: boolean
+ focusHeader: () => void
+ blurHeader: () => void
+ registerOptIn: () => () => void
+}
+
const TabsContext = createContext({
selectedTab: undefined,
width: undefined,
@@ -61,236 +72,248 @@ const TabsContext = createContext({
headerFocused: false,
focusHeader: () => {},
blurHeader: () => {},
- registerOptIn: () => () => {}
-});
-export function Tabs(t0) {
- const $ = _c(25);
- const {
- title,
- color,
- defaultTab,
- children,
- hidden,
- useFullWidth,
- selectedTab: controlledSelectedTab,
- onTabChange,
- banner,
- disableNavigation,
- initialHeaderFocused: t1,
- contentHeight,
- navFromContent: t2
- } = t0;
- const initialHeaderFocused = t1 === undefined ? true : t1;
- const navFromContent = t2 === undefined ? false : t2;
- const {
- columns: terminalWidth
- } = useTerminalSize();
- const tabs = children.map(_temp);
- const defaultTabIndex = defaultTab ? tabs.findIndex(tab => defaultTab === tab[0]) : 0;
- const isControlled = controlledSelectedTab !== undefined;
- const [internalSelectedTab, setInternalSelectedTab] = useState(defaultTabIndex !== -1 ? defaultTabIndex : 0);
- const controlledTabIndex = isControlled ? tabs.findIndex(tab_0 => tab_0[0] === controlledSelectedTab) : -1;
- const selectedTabIndex = isControlled ? controlledTabIndex !== -1 ? controlledTabIndex : 0 : internalSelectedTab;
- const modalScrollRef = useModalScrollRef();
- const [headerFocused, setHeaderFocused] = useState(initialHeaderFocused);
- let t3;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t3 = () => setHeaderFocused(true);
- $[0] = t3;
- } else {
- t3 = $[0];
- }
- const focusHeader = t3;
- let t4;
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
- t4 = () => setHeaderFocused(false);
- $[1] = t4;
- } else {
- t4 = $[1];
- }
- const blurHeader = t4;
- const [optInCount, setOptInCount] = useState(0);
- let t5;
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
- t5 = () => {
- setOptInCount(_temp2);
- return () => setOptInCount(_temp3);
- };
- $[2] = t5;
- } else {
- t5 = $[2];
- }
- const registerOptIn = t5;
- const optedIn = optInCount > 0;
- const handleTabChange = offset => {
- const newIndex = (selectedTabIndex + tabs.length + offset) % tabs.length;
- const newTabId = tabs[newIndex]?.[0];
+ registerOptIn: () => () => {},
+})
+
+export function Tabs({
+ title,
+ color,
+ defaultTab,
+ children,
+ hidden,
+ useFullWidth,
+ selectedTab: controlledSelectedTab,
+ onTabChange,
+ banner,
+ disableNavigation,
+ initialHeaderFocused = true,
+ contentHeight,
+ navFromContent = false,
+}: TabsProps): React.ReactNode {
+ const { columns: terminalWidth } = useTerminalSize()
+ const tabs = children.map(child => [
+ child.props.id ?? child.props.title,
+ child.props.title,
+ ])
+ const defaultTabIndex = defaultTab
+ ? tabs.findIndex(tab => defaultTab === tab[0])
+ : 0
+
+ // Support both controlled and uncontrolled modes
+ const isControlled = controlledSelectedTab !== undefined
+ const [internalSelectedTab, setInternalSelectedTab] = useState(
+ defaultTabIndex !== -1 ? defaultTabIndex : 0,
+ )
+
+ // In controlled mode, find the index of the controlled tab
+ const controlledTabIndex = isControlled
+ ? tabs.findIndex(tab => tab[0] === controlledSelectedTab)
+ : -1
+ const selectedTabIndex = isControlled
+ ? controlledTabIndex !== -1
+ ? controlledTabIndex
+ : 0
+ : internalSelectedTab
+
+ const modalScrollRef = useModalScrollRef()
+
+ // Header focus: left/right/tab only switch tabs when the header row is
+ // focused. Children with interactive content call focusHeader() (via
+ // useTabHeaderFocus) on up-arrow to hand focus back here; down-arrow
+ // returns it. Tabs that never call the hook see no behavior change —
+ // initialHeaderFocused defaults to true so nav always works.
+ const [headerFocused, setHeaderFocused] = useState(initialHeaderFocused)
+ const focusHeader = useCallback(() => setHeaderFocused(true), [])
+ const blurHeader = useCallback(() => setHeaderFocused(false), [])
+ // Count of mounted children using useTabHeaderFocus(). Down-arrow blur and
+ // the ↓ hint only engage when at least one child has opted in — otherwise
+ // pressing down on a legacy tab would strand the user with nav disabled.
+ const [optInCount, setOptInCount] = useState(0)
+ const registerOptIn = useCallback(() => {
+ setOptInCount(n => n + 1)
+ return () => setOptInCount(n => n - 1)
+ }, [])
+ const optedIn = optInCount > 0
+
+ const handleTabChange = (offset: number) => {
+ const newIndex = (selectedTabIndex + tabs.length + offset) % tabs.length
+ const newTabId = tabs[newIndex]?.[0]
+
if (isControlled && onTabChange && newTabId) {
- onTabChange(newTabId);
+ onTabChange(newTabId)
} else {
- setInternalSelectedTab(newIndex);
+ setInternalSelectedTab(newIndex)
}
- setHeaderFocused(true);
- };
- const t6 = !hidden && !disableNavigation && headerFocused;
- let t7;
- if ($[3] !== t6) {
- t7 = {
- context: "Tabs",
- isActive: t6
- };
- $[3] = t6;
- $[4] = t7;
- } else {
- t7 = $[4];
+ // Tab switching is a header action — stay focused so the user can keep
+ // cycling. The newly mounted tab can blur via its own interaction.
+ setHeaderFocused(true)
}
- useKeybindings({
- "tabs:next": () => handleTabChange(1),
- "tabs:previous": () => handleTabChange(-1)
- }, t7);
- let t8;
- if ($[5] !== headerFocused || $[6] !== hidden || $[7] !== optedIn) {
- t8 = e => {
- if (!headerFocused || !optedIn || hidden) {
- return;
- }
- if (e.key === "down") {
- e.preventDefault();
- setHeaderFocused(false);
- }
- };
- $[5] = headerFocused;
- $[6] = hidden;
- $[7] = optedIn;
- $[8] = t8;
- } else {
- t8 = $[8];
- }
- const handleKeyDown = t8;
- const t9 = navFromContent && !headerFocused && optedIn && !hidden && !disableNavigation;
- let t10;
- if ($[9] !== t9) {
- t10 = {
- context: "Tabs",
- isActive: t9
- };
- $[9] = t9;
- $[10] = t10;
- } else {
- t10 = $[10];
- }
- useKeybindings({
- "tabs:next": () => {
- handleTabChange(1);
- setHeaderFocused(true);
+
+ useKeybindings(
+ {
+ 'tabs:next': () => handleTabChange(1),
+ 'tabs:previous': () => handleTabChange(-1),
},
- "tabs:previous": () => {
- handleTabChange(-1);
- setHeaderFocused(true);
+ {
+ context: 'Tabs',
+ isActive: !hidden && !disableNavigation && headerFocused,
+ },
+ )
+
+ // When the header is focused, down-arrow returns focus to content. Only
+ // active when the selected tab has opted in via useTabHeaderFocus() —
+ // legacy tabs have nowhere to return focus to.
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (!headerFocused || !optedIn || hidden) return
+ if (e.key === 'down') {
+ e.preventDefault()
+ setHeaderFocused(false)
}
- }, t10);
- const titleWidth = title ? stringWidth(title) + 1 : 0;
- const tabsWidth = tabs.reduce(_temp4, 0);
- const usedWidth = titleWidth + tabsWidth;
- const spacerWidth = useFullWidth ? Math.max(0, terminalWidth - usedWidth) : 0;
- const contentWidth = useFullWidth ? terminalWidth : undefined;
- const T0 = Box;
- const t11 = "column";
- const t12 = 0;
- const t13 = true;
- const t14 = modalScrollRef ? 0 : undefined;
- const t15 = !hidden && {title !== undefined && {title}}{tabs.map((t16, i) => {
- const [id, title_0] = t16;
- const isCurrent = selectedTabIndex === i;
- const hasColorCursor = color && isCurrent && headerFocused;
- return {" "}{title_0}{" "};
- })}{spacerWidth > 0 && {" ".repeat(spacerWidth)}};
- let t17;
- if ($[11] !== children || $[12] !== contentHeight || $[13] !== contentWidth || $[14] !== hidden || $[15] !== modalScrollRef || $[16] !== selectedTabIndex) {
- t17 = modalScrollRef ? {children} : {children};
- $[11] = children;
- $[12] = contentHeight;
- $[13] = contentWidth;
- $[14] = hidden;
- $[15] = modalScrollRef;
- $[16] = selectedTabIndex;
- $[17] = t17;
- } else {
- t17 = $[17];
}
- let t18;
- if ($[18] !== T0 || $[19] !== banner || $[20] !== handleKeyDown || $[21] !== t14 || $[22] !== t15 || $[23] !== t17) {
- t18 = {t15}{banner}{t17};
- $[18] = T0;
- $[19] = banner;
- $[20] = handleKeyDown;
- $[21] = t14;
- $[22] = t15;
- $[23] = t17;
- $[24] = t18;
- } else {
- t18 = $[24];
- }
- return {t18};
-}
-function _temp4(sum, t0) {
- const [, tabTitle] = t0;
- return sum + (tabTitle ? stringWidth(tabTitle) : 0) + 2 + 1;
-}
-function _temp3(n_0) {
- return n_0 - 1;
-}
-function _temp2(n) {
- return n + 1;
-}
-function _temp(child) {
- return [child.props.id ?? child.props.title, child.props.title];
+
+ // Opt-in: same tabs:next/previous actions, active from content. Focuses
+ // the header so subsequent presses cycle via the handler above.
+ useKeybindings(
+ {
+ 'tabs:next': () => {
+ handleTabChange(1)
+ setHeaderFocused(true)
+ },
+ 'tabs:previous': () => {
+ handleTabChange(-1)
+ setHeaderFocused(true)
+ },
+ },
+ {
+ context: 'Tabs',
+ isActive:
+ navFromContent &&
+ !headerFocused &&
+ optedIn &&
+ !hidden &&
+ !disableNavigation,
+ },
+ )
+
+ // Calculate spacing to fill the available width. No keyboard hint in the
+ // header row — content footers own hints (see useTabHeaderFocus docs).
+ const titleWidth = title ? stringWidth(title) + 1 : 0 // +1 for gap
+ const tabsWidth = tabs.reduce(
+ (sum, [, tabTitle]) => sum + (tabTitle ? stringWidth(tabTitle) : 0) + 2 + 1, // +2 for padding, +1 for gap
+ 0,
+ )
+ const usedWidth = titleWidth + tabsWidth
+ const spacerWidth = useFullWidth ? Math.max(0, terminalWidth - usedWidth) : 0
+
+ const contentWidth = useFullWidth ? terminalWidth : undefined
+
+ return (
+
+
+ {!hidden && (
+
+ {title !== undefined && (
+
+ {title}
+
+ )}
+ {tabs.map(([id, title], i) => {
+ const isCurrent = selectedTabIndex === i
+ const hasColorCursor = color && isCurrent && headerFocused
+ return (
+
+ {' '}
+ {title}{' '}
+
+ )
+ })}
+ {spacerWidth > 0 && {' '.repeat(spacerWidth)}}
+
+ )}
+ {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.
+
+
+ {children}
+
+
+ ) : (
+
+ {children}
+
+ )}
+
+
+ )
}
+
type TabProps = {
- title: string;
- id?: string;
- children: React.ReactNode;
-};
-export function Tab(t0) {
- const $ = _c(4);
- const {
- title,
- id,
- children
- } = t0;
- const {
- selectedTab,
- width
- } = useContext(TabsContext);
- const insideModal = useIsInsideModal();
- if (selectedTab !== (id ?? title)) {
- return null;
- }
- const t1 = insideModal ? 0 : undefined;
- let t2;
- if ($[0] !== children || $[1] !== t1 || $[2] !== width) {
- t2 = {children};
- $[0] = children;
- $[1] = t1;
- $[2] = width;
- $[3] = t2;
- } else {
- t2 = $[3];
- }
- return t2;
+ title: string
+ id?: string
+ children: React.ReactNode
}
-export function useTabsWidth() {
- const {
- width
- } = useContext(TabsContext);
- return width;
+
+export function Tab({ title, id, children }: TabProps): React.ReactNode {
+ const { selectedTab, width } = useContext(TabsContext)
+ const insideModal = useIsInsideModal()
+ if (selectedTab !== (id ?? title)) {
+ return null
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useTabsWidth(): number | undefined {
+ const { width } = useContext(TabsContext)
+ return width
}
/**
@@ -304,36 +327,13 @@ export function useTabsWidth() {
* no onUpFromFirstItem to recover. Split the component so the hook only runs
* when the Select renders.
*/
-export function useTabHeaderFocus() {
- const $ = _c(6);
- const {
- headerFocused,
- focusHeader,
- blurHeader,
- registerOptIn
- } = useContext(TabsContext);
- let t0;
- if ($[0] !== registerOptIn) {
- t0 = [registerOptIn];
- $[0] = registerOptIn;
- $[1] = t0;
- } else {
- t0 = $[1];
- }
- useEffect(registerOptIn, t0);
- let t1;
- if ($[2] !== blurHeader || $[3] !== focusHeader || $[4] !== headerFocused) {
- t1 = {
- headerFocused,
- focusHeader,
- blurHeader
- };
- $[2] = blurHeader;
- $[3] = focusHeader;
- $[4] = headerFocused;
- $[5] = t1;
- } else {
- t1 = $[5];
- }
- return t1;
+export function useTabHeaderFocus(): {
+ headerFocused: boolean
+ focusHeader: () => void
+ blurHeader: () => void
+} {
+ const { headerFocused, focusHeader, blurHeader, registerOptIn } =
+ useContext(TabsContext)
+ useEffect(registerOptIn, [registerOptIn])
+ return { headerFocused, focusHeader, blurHeader }
}
diff --git a/src/components/design-system/ThemeProvider.tsx b/src/components/design-system/ThemeProvider.tsx
index 373f73072..ef60d23a1 100644
--- a/src/components/design-system/ThemeProvider.tsx
+++ b/src/components/design-system/ThemeProvider.tsx
@@ -1,169 +1,160 @@
-import { c as _c } from "react/compiler-runtime";
-import { feature } from 'bun:bundle';
-import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
-import useStdin from '../../ink/hooks/use-stdin.js';
-import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
-import { getSystemThemeName, type SystemTheme } from '../../utils/systemTheme.js';
-import type { ThemeName, ThemeSetting } from '../../utils/theme.js';
+import { feature } from 'bun:bundle'
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react'
+import useStdin from '../../ink/hooks/use-stdin.js'
+import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
+import {
+ getSystemThemeName,
+ type SystemTheme,
+} from '../../utils/systemTheme.js'
+import type { ThemeName, ThemeSetting } from '../../utils/theme.js'
+
type ThemeContextValue = {
/** The saved user preference. May be 'auto'. */
- themeSetting: ThemeSetting;
- setThemeSetting: (setting: ThemeSetting) => void;
- setPreviewTheme: (setting: ThemeSetting) => void;
- savePreview: () => void;
- cancelPreview: () => void;
+ themeSetting: ThemeSetting
+ setThemeSetting: (setting: ThemeSetting) => void
+ setPreviewTheme: (setting: ThemeSetting) => void
+ savePreview: () => void
+ cancelPreview: () => void
/** The resolved theme to render with. Never 'auto'. */
- currentTheme: ThemeName;
-};
+ currentTheme: ThemeName
+}
// Non-'auto' default so useTheme() works without a provider (tests, tooling).
-const DEFAULT_THEME: ThemeName = 'dark';
+const DEFAULT_THEME: ThemeName = 'dark'
+
const ThemeContext = createContext({
themeSetting: DEFAULT_THEME,
setThemeSetting: () => {},
setPreviewTheme: () => {},
savePreview: () => {},
cancelPreview: () => {},
- currentTheme: DEFAULT_THEME
-});
+ currentTheme: DEFAULT_THEME,
+})
+
type Props = {
- children: React.ReactNode;
- initialState?: ThemeSetting;
- onThemeSave?: (setting: ThemeSetting) => void;
-};
+ children: React.ReactNode
+ initialState?: ThemeSetting
+ onThemeSave?: (setting: ThemeSetting) => void
+}
+
function defaultInitialTheme(): ThemeSetting {
- return getGlobalConfig().theme;
+ return getGlobalConfig().theme
}
+
function defaultSaveTheme(setting: ThemeSetting): void {
- saveGlobalConfig(current => ({
- ...current,
- theme: setting
- }));
+ saveGlobalConfig(current => ({ ...current, theme: setting }))
}
+
export function ThemeProvider({
children,
initialState,
- onThemeSave = defaultSaveTheme
+ onThemeSave = defaultSaveTheme,
}: Props) {
- const [themeSetting, setThemeSetting] = useState(initialState ?? defaultInitialTheme);
- const [previewTheme, setPreviewTheme] = useState(null);
+ const [themeSetting, setThemeSetting] = useState(
+ initialState ?? defaultInitialTheme,
+ )
+ const [previewTheme, setPreviewTheme] = useState(null)
// Track terminal theme for 'auto' resolution. Seeds from $COLORFGBG (or
// 'dark' if unset); the OSC 11 watcher corrects it on first poll.
- const [systemTheme, setSystemTheme] = useState(() => (initialState ?? themeSetting) === 'auto' ? getSystemThemeName() : 'dark');
+ const [systemTheme, setSystemTheme] = useState(() =>
+ (initialState ?? themeSetting) === 'auto' ? getSystemThemeName() : 'dark',
+ )
// The setting currently in effect (preview wins while picker is open)
- const activeSetting = previewTheme ?? themeSetting;
- const {
- internal_querier
- } = useStdin();
+ const activeSetting = previewTheme ?? themeSetting
+
+ const { internal_querier } = useStdin()
// Watch for live terminal theme changes while 'auto' is active.
// Positive feature() pattern so the watcher import is dead-code-eliminated
// in external builds.
useEffect(() => {
if (feature('AUTO_THEME')) {
- if (activeSetting !== 'auto' || !internal_querier) return;
- let cleanup: (() => void) | undefined;
- let cancelled = false;
- void import('../../utils/systemThemeWatcher.js').then(({
- watchSystemTheme
- }) => {
- if (cancelled) return;
- cleanup = watchSystemTheme(internal_querier, setSystemTheme);
- });
+ if (activeSetting !== 'auto' || !internal_querier) return
+ let cleanup: (() => void) | undefined
+ let cancelled = false
+ void import('../../utils/systemThemeWatcher.js').then(
+ ({ watchSystemTheme }) => {
+ if (cancelled) return
+ cleanup = watchSystemTheme(internal_querier, setSystemTheme)
+ },
+ )
return () => {
- cancelled = true;
- cleanup?.();
- };
+ cancelled = true
+ cleanup?.()
+ }
}
- }, [activeSetting, internal_querier]);
- const currentTheme: ThemeName = activeSetting === 'auto' ? systemTheme : activeSetting;
- const value = useMemo(() => ({
- themeSetting,
- setThemeSetting: (newSetting: ThemeSetting) => {
- setThemeSetting(newSetting);
- setPreviewTheme(null);
- // Switching to 'auto' restarts the watcher (activeSetting dep), whose
- // first poll fires immediately. Seed from the cache so the OSC
- // round-trip doesn't flash the wrong palette.
- if (newSetting === 'auto') {
- setSystemTheme(getSystemThemeName());
- }
- onThemeSave?.(newSetting);
- },
- setPreviewTheme: (newSetting_0: ThemeSetting) => {
- setPreviewTheme(newSetting_0);
- if (newSetting_0 === 'auto') {
- setSystemTheme(getSystemThemeName());
- }
- },
- savePreview: () => {
- if (previewTheme !== null) {
- setThemeSetting(previewTheme);
- setPreviewTheme(null);
- onThemeSave?.(previewTheme);
- }
- },
- cancelPreview: () => {
- if (previewTheme !== null) {
- setPreviewTheme(null);
- }
- },
- currentTheme
- }), [themeSetting, previewTheme, currentTheme, onThemeSave]);
- return {children};
+ }, [activeSetting, internal_querier])
+
+ const currentTheme: ThemeName =
+ activeSetting === 'auto' ? systemTheme : activeSetting
+
+ const value = useMemo(
+ () => ({
+ themeSetting,
+ setThemeSetting: (newSetting: ThemeSetting) => {
+ setThemeSetting(newSetting)
+ setPreviewTheme(null)
+ // Switching to 'auto' restarts the watcher (activeSetting dep), whose
+ // first poll fires immediately. Seed from the cache so the OSC
+ // round-trip doesn't flash the wrong palette.
+ if (newSetting === 'auto') {
+ setSystemTheme(getSystemThemeName())
+ }
+ onThemeSave?.(newSetting)
+ },
+ setPreviewTheme: (newSetting: ThemeSetting) => {
+ setPreviewTheme(newSetting)
+ if (newSetting === 'auto') {
+ setSystemTheme(getSystemThemeName())
+ }
+ },
+ savePreview: () => {
+ if (previewTheme !== null) {
+ setThemeSetting(previewTheme)
+ setPreviewTheme(null)
+ onThemeSave?.(previewTheme)
+ }
+ },
+ cancelPreview: () => {
+ if (previewTheme !== null) {
+ setPreviewTheme(null)
+ }
+ },
+ currentTheme,
+ }),
+ [themeSetting, previewTheme, currentTheme, onThemeSave],
+ )
+
+ return {children}
}
/**
* Returns the resolved theme for rendering (never 'auto') and a setter that
* accepts any ThemeSetting (including 'auto').
*/
-export function useTheme() {
- const $ = _c(3);
- const {
- currentTheme,
- setThemeSetting
- } = useContext(ThemeContext);
- let t0;
- if ($[0] !== currentTheme || $[1] !== setThemeSetting) {
- t0 = [currentTheme, setThemeSetting];
- $[0] = currentTheme;
- $[1] = setThemeSetting;
- $[2] = t0;
- } else {
- t0 = $[2];
- }
- return t0;
+export function useTheme(): [ThemeName, (setting: ThemeSetting) => void] {
+ const { currentTheme, setThemeSetting } = useContext(ThemeContext)
+ return [currentTheme, setThemeSetting]
}
/**
* Returns the raw theme setting as stored in config. Use this in UI that
* needs to show 'auto' as a distinct choice (e.g., ThemePicker).
*/
-export function useThemeSetting() {
- return useContext(ThemeContext).themeSetting;
+export function useThemeSetting(): ThemeSetting {
+ return useContext(ThemeContext).themeSetting
}
+
export function usePreviewTheme() {
- const $ = _c(4);
- const {
- setPreviewTheme,
- savePreview,
- cancelPreview
- } = useContext(ThemeContext);
- let t0;
- if ($[0] !== cancelPreview || $[1] !== savePreview || $[2] !== setPreviewTheme) {
- t0 = {
- setPreviewTheme,
- savePreview,
- cancelPreview
- };
- $[0] = cancelPreview;
- $[1] = savePreview;
- $[2] = setPreviewTheme;
- $[3] = t0;
- } else {
- t0 = $[3];
- }
- return t0;
+ const { setPreviewTheme, savePreview, cancelPreview } =
+ useContext(ThemeContext)
+ return { setPreviewTheme, savePreview, cancelPreview }
}
diff --git a/src/components/design-system/ThemedBox.tsx b/src/components/design-system/ThemedBox.tsx
index 0b56f18a6..10fbe9137 100644
--- a/src/components/design-system/ThemedBox.tsx
+++ b/src/components/design-system/ThemedBox.tsx
@@ -1,155 +1,112 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { type PropsWithChildren, type Ref } from 'react';
-import Box from '../../ink/components/Box.js';
-import type { DOMElement } from '../../ink/dom.js';
-import type { ClickEvent } from '../../ink/events/click-event.js';
-import type { FocusEvent } from '../../ink/events/focus-event.js';
-import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
-import type { Color, Styles } from '../../ink/styles.js';
-import { getTheme, type Theme } from '../../utils/theme.js';
-import { useTheme } from './ThemeProvider.js';
+import React, { type PropsWithChildren, type Ref } from 'react'
+import Box from '../../ink/components/Box.js'
+import type { DOMElement } from '../../ink/dom.js'
+import type { ClickEvent } from '../../ink/events/click-event.js'
+import type { FocusEvent } from '../../ink/events/focus-event.js'
+import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
+import type { Color, Styles } from '../../ink/styles.js'
+import { getTheme, type Theme } from '../../utils/theme.js'
+import { useTheme } from './ThemeProvider.js'
// Color props that accept theme keys
type ThemedColorProps = {
- readonly borderColor?: keyof Theme | Color;
- readonly borderTopColor?: keyof Theme | Color;
- readonly borderBottomColor?: keyof Theme | Color;
- readonly borderLeftColor?: keyof Theme | Color;
- readonly borderRightColor?: keyof Theme | Color;
- readonly backgroundColor?: keyof Theme | Color;
-};
+ readonly borderColor?: keyof Theme | Color
+ readonly borderTopColor?: keyof Theme | Color
+ readonly borderBottomColor?: keyof Theme | Color
+ readonly borderLeftColor?: keyof Theme | Color
+ readonly borderRightColor?: keyof Theme | Color
+ readonly backgroundColor?: keyof Theme | Color
+}
// Base Styles without color props (they'll be overridden)
-type BaseStylesWithoutColors = Omit;
-export type Props = BaseStylesWithoutColors & ThemedColorProps & {
- ref?: Ref;
- tabIndex?: number;
- autoFocus?: boolean;
- onClick?: (event: ClickEvent) => void;
- onFocus?: (event: FocusEvent) => void;
- onFocusCapture?: (event: FocusEvent) => void;
- onBlur?: (event: FocusEvent) => void;
- onBlurCapture?: (event: FocusEvent) => void;
- onKeyDown?: (event: KeyboardEvent) => void;
- onKeyDownCapture?: (event: KeyboardEvent) => void;
- onMouseEnter?: () => void;
- onMouseLeave?: () => void;
-};
+type BaseStylesWithoutColors = Omit<
+ Styles,
+ | 'textWrap'
+ | 'borderColor'
+ | 'borderTopColor'
+ | 'borderBottomColor'
+ | 'borderLeftColor'
+ | 'borderRightColor'
+ | 'backgroundColor'
+>
+
+export type Props = BaseStylesWithoutColors &
+ ThemedColorProps & {
+ ref?: Ref
+ tabIndex?: number
+ autoFocus?: boolean
+ onClick?: (event: ClickEvent) => void
+ onFocus?: (event: FocusEvent) => void
+ onFocusCapture?: (event: FocusEvent) => void
+ onBlur?: (event: FocusEvent) => void
+ onBlurCapture?: (event: FocusEvent) => void
+ onKeyDown?: (event: KeyboardEvent) => void
+ onKeyDownCapture?: (event: KeyboardEvent) => void
+ onMouseEnter?: () => void
+ onMouseLeave?: () => void
+ }
/**
* Resolves a color value that may be a theme key to a raw Color.
*/
-function resolveColor(color: keyof Theme | Color | undefined, theme: Theme): Color | undefined {
- if (!color) return undefined;
+function resolveColor(
+ color: keyof Theme | Color | undefined,
+ theme: Theme,
+): Color | undefined {
+ if (!color) return undefined
// Check if it's a raw color (starts with rgb(, #, ansi256(, or ansi:)
- if (color.startsWith('rgb(') || color.startsWith('#') || color.startsWith('ansi256(') || color.startsWith('ansi:')) {
- return color as Color;
+ if (
+ color.startsWith('rgb(') ||
+ color.startsWith('#') ||
+ color.startsWith('ansi256(') ||
+ color.startsWith('ansi:')
+ ) {
+ return color as Color
}
// It's a theme key - resolve it
- return theme[color as keyof Theme] as Color;
+ return theme[color as keyof Theme] as Color
}
/**
* Theme-aware Box component that resolves theme color keys to raw colors.
* This wraps the base Box component with theme resolution for border colors.
*/
-function ThemedBox(t0) {
- const $ = _c(33);
- let backgroundColor;
- let borderBottomColor;
- let borderColor;
- let borderLeftColor;
- let borderRightColor;
- let borderTopColor;
- let children;
- let ref;
- let rest;
- if ($[0] !== t0) {
- ({
- borderColor,
- borderTopColor,
- borderBottomColor,
- borderLeftColor,
- borderRightColor,
- backgroundColor,
- children,
- ref,
- ...rest
- } = t0);
- $[0] = t0;
- $[1] = backgroundColor;
- $[2] = borderBottomColor;
- $[3] = borderColor;
- $[4] = borderLeftColor;
- $[5] = borderRightColor;
- $[6] = borderTopColor;
- $[7] = children;
- $[8] = ref;
- $[9] = rest;
- } else {
- backgroundColor = $[1];
- borderBottomColor = $[2];
- borderColor = $[3];
- borderLeftColor = $[4];
- borderRightColor = $[5];
- borderTopColor = $[6];
- children = $[7];
- ref = $[8];
- rest = $[9];
- }
- const [themeName] = useTheme();
- let resolvedBorderBottomColor;
- let resolvedBorderColor;
- let resolvedBorderLeftColor;
- let resolvedBorderRightColor;
- let resolvedBorderTopColor;
- let t1;
- if ($[10] !== backgroundColor || $[11] !== borderBottomColor || $[12] !== borderColor || $[13] !== borderLeftColor || $[14] !== borderRightColor || $[15] !== borderTopColor || $[16] !== themeName) {
- const theme = getTheme(themeName);
- resolvedBorderColor = resolveColor(borderColor, theme);
- resolvedBorderTopColor = resolveColor(borderTopColor, theme);
- resolvedBorderBottomColor = resolveColor(borderBottomColor, theme);
- resolvedBorderLeftColor = resolveColor(borderLeftColor, theme);
- resolvedBorderRightColor = resolveColor(borderRightColor, theme);
- t1 = resolveColor(backgroundColor, theme);
- $[10] = backgroundColor;
- $[11] = borderBottomColor;
- $[12] = borderColor;
- $[13] = borderLeftColor;
- $[14] = borderRightColor;
- $[15] = borderTopColor;
- $[16] = themeName;
- $[17] = resolvedBorderBottomColor;
- $[18] = resolvedBorderColor;
- $[19] = resolvedBorderLeftColor;
- $[20] = resolvedBorderRightColor;
- $[21] = resolvedBorderTopColor;
- $[22] = t1;
- } else {
- resolvedBorderBottomColor = $[17];
- resolvedBorderColor = $[18];
- resolvedBorderLeftColor = $[19];
- resolvedBorderRightColor = $[20];
- resolvedBorderTopColor = $[21];
- t1 = $[22];
- }
- const resolvedBackgroundColor = t1;
- let t2;
- if ($[23] !== children || $[24] !== ref || $[25] !== resolvedBackgroundColor || $[26] !== resolvedBorderBottomColor || $[27] !== resolvedBorderColor || $[28] !== resolvedBorderLeftColor || $[29] !== resolvedBorderRightColor || $[30] !== resolvedBorderTopColor || $[31] !== rest) {
- t2 = {children};
- $[23] = children;
- $[24] = ref;
- $[25] = resolvedBackgroundColor;
- $[26] = resolvedBorderBottomColor;
- $[27] = resolvedBorderColor;
- $[28] = resolvedBorderLeftColor;
- $[29] = resolvedBorderRightColor;
- $[30] = resolvedBorderTopColor;
- $[31] = rest;
- $[32] = t2;
- } else {
- t2 = $[32];
- }
- return t2;
+function ThemedBox({
+ borderColor,
+ borderTopColor,
+ borderBottomColor,
+ borderLeftColor,
+ borderRightColor,
+ backgroundColor,
+ children,
+ ref,
+ ...rest
+}: PropsWithChildren): React.ReactNode {
+ const [themeName] = useTheme()
+ const theme = getTheme(themeName)
+
+ // Resolve theme keys to raw colors
+ const resolvedBorderColor = resolveColor(borderColor, theme)
+ const resolvedBorderTopColor = resolveColor(borderTopColor, theme)
+ const resolvedBorderBottomColor = resolveColor(borderBottomColor, theme)
+ const resolvedBorderLeftColor = resolveColor(borderLeftColor, theme)
+ const resolvedBorderRightColor = resolveColor(borderRightColor, theme)
+ const resolvedBackgroundColor = resolveColor(backgroundColor, theme)
+
+ return (
+
+ {children}
+
+ )
}
-export default ThemedBox;
+
+export default ThemedBox
diff --git a/src/components/design-system/ThemedText.tsx b/src/components/design-system/ThemedText.tsx
index abaa68f23..3c32b8bc3 100644
--- a/src/components/design-system/ThemedText.tsx
+++ b/src/components/design-system/ThemedText.tsx
@@ -1,123 +1,132 @@
-import { c as _c } from "react/compiler-runtime";
-import type { ReactNode } from 'react';
-import React, { useContext } from 'react';
-import Text from '../../ink/components/Text.js';
-import type { Color, Styles } from '../../ink/styles.js';
-import { getTheme, type Theme } from '../../utils/theme.js';
-import { useTheme } from './ThemeProvider.js';
+import type { ReactNode } from 'react'
+import React, { useContext } from 'react'
+import Text from '../../ink/components/Text.js'
+import type { Color, Styles } from '../../ink/styles.js'
+import { getTheme, type Theme } from '../../utils/theme.js'
+import { useTheme } from './ThemeProvider.js'
/** Colors uncolored ThemedText in the subtree. Precedence: explicit `color` >
* this > dimColor. Crosses Box boundaries (Ink's style cascade doesn't). */
-export const TextHoverColorContext = React.createContext(undefined);
+export const TextHoverColorContext = React.createContext<
+ keyof Theme | undefined
+>(undefined)
+
export type Props = {
/**
* Change text color. Accepts a theme key or raw color value.
*/
- readonly color?: keyof Theme | Color;
+ readonly color?: keyof Theme | Color
/**
* Same as `color`, but for background. Must be a theme key.
*/
- readonly backgroundColor?: keyof Theme;
+ readonly backgroundColor?: keyof Theme
/**
* Dim the color using the theme's inactive color.
* This is compatible with bold (unlike ANSI dim).
*/
- readonly dimColor?: boolean;
+ readonly dimColor?: boolean
/**
* Make the text bold.
*/
- readonly bold?: boolean;
+ readonly bold?: boolean
/**
* Make the text italic.
*/
- readonly italic?: boolean;
+ readonly italic?: boolean
/**
* Make the text underlined.
*/
- readonly underline?: boolean;
+ readonly underline?: boolean
/**
* Make the text crossed with a line.
*/
- readonly strikethrough?: boolean;
+ readonly strikethrough?: boolean
/**
* Inverse background and foreground colors.
*/
- readonly inverse?: boolean;
+ readonly inverse?: boolean
/**
* This property tells Ink to wrap or truncate text if its width is larger than container.
* If `wrap` is passed (by default), Ink will wrap text and split it into multiple lines.
* If `truncate-*` is passed, Ink will truncate text instead, which will result in one line of text with the rest cut off.
*/
- readonly wrap?: Styles['textWrap'];
- readonly children?: ReactNode;
-};
+ readonly wrap?: Styles['textWrap']
+
+ readonly children?: ReactNode
+}
/**
* Resolves a color value that may be a theme key to a raw Color.
*/
-function resolveColor(color: keyof Theme | Color | undefined, theme: Theme): Color | undefined {
- if (!color) return undefined;
+function resolveColor(
+ color: keyof Theme | Color | undefined,
+ theme: Theme,
+): Color | undefined {
+ if (!color) return undefined
// Check if it's a raw color (starts with rgb(, #, ansi256(, or ansi:)
- if (color.startsWith('rgb(') || color.startsWith('#') || color.startsWith('ansi256(') || color.startsWith('ansi:')) {
- return color as Color;
+ if (
+ color.startsWith('rgb(') ||
+ color.startsWith('#') ||
+ color.startsWith('ansi256(') ||
+ color.startsWith('ansi:')
+ ) {
+ return color as Color
}
// It's a theme key - resolve it
- return theme[color as keyof Theme] as Color;
+ return theme[color as keyof Theme] as Color
}
/**
* Theme-aware Text component that resolves theme color keys to raw colors.
* This wraps the base Text component with theme resolution.
*/
-export default function ThemedText(t0) {
- const $ = _c(10);
- const {
- color,
- backgroundColor,
- dimColor: t1,
- bold: t2,
- italic: t3,
- underline: t4,
- strikethrough: t5,
- inverse: t6,
- wrap: t7,
- children
- } = t0;
- const dimColor = t1 === undefined ? false : t1;
- const bold = t2 === undefined ? false : t2;
- const italic = t3 === undefined ? false : t3;
- const underline = t4 === undefined ? false : t4;
- const strikethrough = t5 === undefined ? false : t5;
- const inverse = t6 === undefined ? false : t6;
- const wrap = t7 === undefined ? "wrap" : t7;
- const [themeName] = useTheme();
- const theme = getTheme(themeName);
- const hoverColor = useContext(TextHoverColorContext);
- const resolvedColor = !color && hoverColor ? resolveColor(hoverColor, theme) : dimColor ? theme.inactive as Color : resolveColor(color, theme);
- const resolvedBackgroundColor = backgroundColor ? theme[backgroundColor] as Color : undefined;
- let t8;
- if ($[0] !== bold || $[1] !== children || $[2] !== inverse || $[3] !== italic || $[4] !== resolvedBackgroundColor || $[5] !== resolvedColor || $[6] !== strikethrough || $[7] !== underline || $[8] !== wrap) {
- t8 = {children};
- $[0] = bold;
- $[1] = children;
- $[2] = inverse;
- $[3] = italic;
- $[4] = resolvedBackgroundColor;
- $[5] = resolvedColor;
- $[6] = strikethrough;
- $[7] = underline;
- $[8] = wrap;
- $[9] = t8;
- } else {
- t8 = $[9];
- }
- return t8;
+export default function ThemedText({
+ color,
+ backgroundColor,
+ dimColor = false,
+ bold = false,
+ italic = false,
+ underline = false,
+ strikethrough = false,
+ inverse = false,
+ wrap = 'wrap',
+ children,
+}: Props): React.ReactNode {
+ const [themeName] = useTheme()
+ const theme = getTheme(themeName)
+ const hoverColor = useContext(TextHoverColorContext)
+
+ // Resolve theme keys to raw colors
+ const resolvedColor =
+ !color && hoverColor
+ ? resolveColor(hoverColor, theme)
+ : dimColor
+ ? (theme.inactive as Color)
+ : resolveColor(color, theme)
+ const resolvedBackgroundColor = backgroundColor
+ ? (theme[backgroundColor] as Color)
+ : undefined
+
+ return (
+
+ {children}
+
+ )
}
diff --git a/src/components/skills/SkillsMenu.tsx b/src/components/skills/SkillsMenu.tsx
index df78c1af4..f8f2896a6 100644
--- a/src/components/skills/SkillsMenu.tsx
+++ b/src/components/skills/SkillsMenu.tsx
@@ -1,236 +1,205 @@
-import { c as _c } from "react/compiler-runtime";
-import capitalize from 'lodash-es/capitalize.js';
-import * as React from 'react';
-import { useMemo } from 'react';
-import { type Command, type CommandBase, type CommandResultDisplay, getCommandName, type PromptCommand } from '../../commands.js';
-import { Box, Text } from '../../ink.js';
-import { estimateSkillFrontmatterTokens, getSkillsPath } from '../../skills/loadSkillsDir.js';
-import { getDisplayPath } from '../../utils/file.js';
-import { formatTokens } from '../../utils/format.js';
-import { getSettingSourceName, type SettingSource } from '../../utils/settings/constants.js';
-import { plural } from '../../utils/stringUtils.js';
-import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
-import { Dialog } from '../design-system/Dialog.js';
+import capitalize from 'lodash-es/capitalize.js'
+import * as React from 'react'
+import { useMemo } from 'react'
+import {
+ type Command,
+ type CommandBase,
+ type CommandResultDisplay,
+ getCommandName,
+ type PromptCommand,
+} from '../../commands.js'
+import { Box, Text } from '../../ink.js'
+import {
+ estimateSkillFrontmatterTokens,
+ getSkillsPath,
+} from '../../skills/loadSkillsDir.js'
+import { getDisplayPath } from '../../utils/file.js'
+import { formatTokens } from '../../utils/format.js'
+import {
+ getSettingSourceName,
+ type SettingSource,
+} from '../../utils/settings/constants.js'
+import { plural } from '../../utils/stringUtils.js'
+import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
+import { Dialog } from '../design-system/Dialog.js'
// Skills are always PromptCommands with CommandBase properties
-type SkillCommand = CommandBase & PromptCommand;
-type SkillSource = SettingSource | 'plugin' | 'mcp';
+type SkillCommand = CommandBase & PromptCommand
+
+type SkillSource = SettingSource | 'plugin' | 'mcp'
+
type Props = {
- onExit: (result?: string, options?: {
- display?: CommandResultDisplay;
- }) => void;
- commands: Command[];
-};
+ onExit: (
+ result?: string,
+ options?: { display?: CommandResultDisplay },
+ ) => void
+ commands: Command[]
+}
+
function getSourceTitle(source: SkillSource): string {
if (source === 'plugin') {
- return 'Plugin skills';
+ return 'Plugin skills'
}
if (source === 'mcp') {
- return 'MCP skills';
+ return 'MCP skills'
}
- return `${capitalize(getSettingSourceName(source))} skills`;
+ return `${capitalize(getSettingSourceName(source))} skills`
}
-function getSourceSubtitle(source: SkillSource, skills: SkillCommand[]): string | undefined {
+
+function getSourceSubtitle(
+ source: SkillSource,
+ skills: SkillCommand[],
+): string | undefined {
// MCP skills show server names; file-based skills show filesystem paths.
// Skill names are `:`, not `mcp____…`.
if (source === 'mcp') {
- const servers = [...new Set(skills.map(s => {
- const idx = s.name.indexOf(':');
- return idx > 0 ? s.name.slice(0, idx) : null;
- }).filter((n): n is string => n != null))];
- return servers.length > 0 ? servers.join(', ') : undefined;
+ const servers = [
+ ...new Set(
+ skills
+ .map(s => {
+ const idx = s.name.indexOf(':')
+ return idx > 0 ? s.name.slice(0, idx) : null
+ })
+ .filter((n): n is string => n != null),
+ ),
+ ]
+ return servers.length > 0 ? servers.join(', ') : undefined
}
- const skillsPath = getDisplayPath(getSkillsPath(source, 'skills'));
- const hasCommandsSkills = skills.some(s => s.loadedFrom === 'commands_DEPRECATED');
- return hasCommandsSkills ? `${skillsPath}, ${getDisplayPath(getSkillsPath(source, 'commands'))}` : skillsPath;
+ const skillsPath = getDisplayPath(getSkillsPath(source, 'skills'))
+ const hasCommandsSkills = skills.some(
+ s => s.loadedFrom === 'commands_DEPRECATED',
+ )
+ return hasCommandsSkills
+ ? `${skillsPath}, ${getDisplayPath(getSkillsPath(source, 'commands'))}`
+ : skillsPath
}
-export function SkillsMenu(t0) {
- const $ = _c(35);
- const {
- onExit,
- commands
- } = t0;
- let t1;
- if ($[0] !== commands) {
- t1 = commands.filter(_temp);
- $[0] = commands;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- const skills = t1;
- let groups;
- if ($[2] !== skills) {
- groups = {
+
+export function SkillsMenu({ onExit, commands }: Props): React.ReactNode {
+ // Filter commands for skills and cast to SkillCommand
+ const skills = useMemo(() => {
+ return commands.filter(
+ (cmd): cmd is SkillCommand =>
+ cmd.type === 'prompt' &&
+ (cmd.loadedFrom === 'skills' ||
+ cmd.loadedFrom === 'commands_DEPRECATED' ||
+ cmd.loadedFrom === 'plugin' ||
+ cmd.loadedFrom === 'mcp'),
+ )
+ }, [commands])
+
+ const skillsBySource = useMemo((): Record => {
+ const groups: Record = {
policySettings: [],
userSettings: [],
projectSettings: [],
localSettings: [],
flagSettings: [],
plugin: [],
- mcp: []
- };
+ mcp: [],
+ }
+
for (const skill of skills) {
- const source = skill.source as SkillSource;
+ const source = skill.source as SkillSource
if (source in groups) {
- groups[source].push(skill);
+ groups[source].push(skill)
}
}
+
for (const group of Object.values(groups)) {
- (group as Array<{ name: string }>).sort(_temp2);
+ group.sort((a, b) => getCommandName(a).localeCompare(getCommandName(b)))
}
- $[2] = skills;
- $[3] = groups;
- } else {
- groups = $[3];
+
+ return groups
+ }, [skills])
+
+ const handleCancel = (): void => {
+ onExit('Skills dialog dismissed', { display: 'system' })
}
- const skillsBySource = groups;
- let t2;
- if ($[4] !== onExit) {
- t2 = () => {
- onExit("Skills dialog dismissed", {
- display: "system"
- });
- };
- $[4] = onExit;
- $[5] = t2;
- } else {
- t2 = $[5];
- }
- const handleCancel = t2;
+
if (skills.length === 0) {
- let t3;
- if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
- t3 = Create skills in .claude/skills/ or ~/.claude/skills/;
- $[6] = t3;
- } else {
- t3 = $[6];
- }
- let t4;
- if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
- t4 = ;
- $[7] = t4;
- } else {
- t4 = $[7];
- }
- let t5;
- if ($[8] !== handleCancel) {
- t5 = {t3}{t4};
- $[8] = handleCancel;
- $[9] = t5;
- } else {
- t5 = $[9];
- }
- return t5;
+ return (
+
+
+ Create skills in .claude/skills/ or ~/.claude/skills/
+
+
+
+
+
+ )
}
- const renderSkill = _temp3;
- let t3;
- if ($[10] !== skillsBySource) {
- t3 = source_0 => {
- const groupSkills = skillsBySource[source_0];
- if (groupSkills.length === 0) {
- return null;
- }
- const title = getSourceTitle(source_0);
- const subtitle = getSourceSubtitle(source_0, groupSkills);
- return {title}{subtitle && ({subtitle})}{groupSkills.map(skill_1 => renderSkill(skill_1))};
- };
- $[10] = skillsBySource;
- $[11] = t3;
- } else {
- t3 = $[11];
+
+ const renderSkill = (skill: SkillCommand) => {
+ const estimatedTokens = estimateSkillFrontmatterTokens(skill)
+ const tokenDisplay = `~${formatTokens(estimatedTokens)}`
+ const pluginName =
+ skill.source === 'plugin'
+ ? skill.pluginInfo?.pluginManifest.name
+ : undefined
+
+ return (
+
+ {getCommandName(skill)}
+
+ {pluginName ? ` · ${pluginName}` : ''} · {tokenDisplay} description
+ tokens
+
+
+ )
}
- const renderSkillGroup = t3;
- const t4 = skills.length;
- let t5;
- if ($[12] !== skills.length) {
- t5 = plural(skills.length, "skill");
- $[12] = skills.length;
- $[13] = t5;
- } else {
- t5 = $[13];
+
+ const renderSkillGroup = (source: SkillSource) => {
+ const groupSkills = skillsBySource[source]
+ if (groupSkills.length === 0) return null
+
+ const title = getSourceTitle(source)
+ const subtitle = getSourceSubtitle(source, groupSkills)
+
+ return (
+
+
+
+ {title}
+
+ {subtitle && ({subtitle})}
+
+ {groupSkills.map(skill => renderSkill(skill))}
+
+ )
}
- const t6 = `${t4} ${t5}`;
- let t7;
- if ($[14] !== renderSkillGroup) {
- t7 = renderSkillGroup("projectSettings");
- $[14] = renderSkillGroup;
- $[15] = t7;
- } else {
- t7 = $[15];
- }
- let t8;
- if ($[16] !== renderSkillGroup) {
- t8 = renderSkillGroup("userSettings");
- $[16] = renderSkillGroup;
- $[17] = t8;
- } else {
- t8 = $[17];
- }
- let t9;
- if ($[18] !== renderSkillGroup) {
- t9 = renderSkillGroup("policySettings");
- $[18] = renderSkillGroup;
- $[19] = t9;
- } else {
- t9 = $[19];
- }
- let t10;
- if ($[20] !== renderSkillGroup) {
- t10 = renderSkillGroup("plugin");
- $[20] = renderSkillGroup;
- $[21] = t10;
- } else {
- t10 = $[21];
- }
- let t11;
- if ($[22] !== renderSkillGroup) {
- t11 = renderSkillGroup("mcp");
- $[22] = renderSkillGroup;
- $[23] = t11;
- } else {
- t11 = $[23];
- }
- let t12;
- if ($[24] !== t10 || $[25] !== t11 || $[26] !== t7 || $[27] !== t8 || $[28] !== t9) {
- t12 = {t7}{t8}{t9}{t10}{t11};
- $[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 = ;
- $[30] = t13;
- } else {
- t13 = $[30];
- }
- let t14;
- if ($[31] !== handleCancel || $[32] !== t12 || $[33] !== t6) {
- t14 = {t12}{t13};
- $[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 {getCommandName(skill_0)}{pluginName ? ` · ${pluginName}` : ""} · {tokenDisplay} description tokens;
-}
-function _temp2(a, b) {
- return getCommandName(a).localeCompare(getCommandName(b));
-}
-function _temp(cmd) {
- return cmd.type === "prompt" && (cmd.loadedFrom === "skills" || cmd.loadedFrom === "commands_DEPRECATED" || cmd.loadedFrom === "plugin" || cmd.loadedFrom === "mcp");
+
+ return (
+
+
+ {renderSkillGroup('projectSettings')}
+ {renderSkillGroup('userSettings')}
+ {renderSkillGroup('policySettings')}
+ {renderSkillGroup('plugin')}
+ {renderSkillGroup('mcp')}
+
+
+
+
+
+ )
}
diff --git a/src/components/tasks/AsyncAgentDetailDialog.tsx b/src/components/tasks/AsyncAgentDetailDialog.tsx
index a942c105e..4174d4fa5 100644
--- a/src/components/tasks/AsyncAgentDetailDialog.tsx
+++ b/src/components/tasks/AsyncAgentDetailDialog.tsx
@@ -1,228 +1,200 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { useMemo } from 'react';
-import type { DeepImmutable } from 'src/types/utils.js';
-import { useElapsedTime } from '../../hooks/useElapsedTime.js';
-import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
-import { Box, Text, useTheme } from '../../ink.js';
-import { useKeybindings } from '../../keybindings/useKeybinding.js';
-import { getEmptyToolPermissionContext } from '../../Tool.js';
-import type { LocalAgentTaskState } from '../../tasks/LocalAgentTask/LocalAgentTask.js';
-import { getTools } from '../../tools.js';
-import { formatNumber } from '../../utils/format.js';
-import { extractTag } from '../../utils/messages.js';
-import { Byline } from '../design-system/Byline.js';
-import { Dialog } from '../design-system/Dialog.js';
-import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
-import { UserPlanMessage } from '../messages/UserPlanMessage.js';
-import { renderToolActivity } from './renderToolActivity.js';
-import { getTaskStatusColor, getTaskStatusIcon } from './taskStatusUtils.js';
+import React, { useMemo } from 'react'
+import type { DeepImmutable } from 'src/types/utils.js'
+import { useElapsedTime } from '../../hooks/useElapsedTime.js'
+import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
+import { Box, Text, useTheme } from '../../ink.js'
+import { useKeybindings } from '../../keybindings/useKeybinding.js'
+import { getEmptyToolPermissionContext } from '../../Tool.js'
+import type { LocalAgentTaskState } from '../../tasks/LocalAgentTask/LocalAgentTask.js'
+import { getTools } from '../../tools.js'
+import { formatNumber } from '../../utils/format.js'
+import { extractTag } from '../../utils/messages.js'
+import { Byline } from '../design-system/Byline.js'
+import { Dialog } from '../design-system/Dialog.js'
+import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
+import { UserPlanMessage } from '../messages/UserPlanMessage.js'
+import { renderToolActivity } from './renderToolActivity.js'
+import { getTaskStatusColor, getTaskStatusIcon } from './taskStatusUtils.js'
+
type Props = {
- agent: DeepImmutable;
- onDone: () => void;
- onKillAgent?: () => void;
- onBack?: () => void;
-};
-export function AsyncAgentDetailDialog(t0) {
- const $ = _c(54);
- const {
- agent,
- onDone,
- onKillAgent,
- onBack
- } = t0;
- const [theme] = useTheme();
- let t1;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = getTools(getEmptyToolPermissionContext());
- $[0] = t1;
- } else {
- t1 = $[0];
- }
- const tools = t1;
- const elapsedTime = useElapsedTime(agent.startTime, agent.status === "running", 1000, agent.totalPausedMs ?? 0);
- let t2;
- if ($[1] !== onDone) {
- t2 = {
- "confirm:yes": onDone
- };
- $[1] = onDone;
- $[2] = t2;
- } else {
- t2 = $[2];
- }
- let t3;
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
- t3 = {
- context: "Confirmation"
- };
- $[3] = t3;
- } else {
- t3 = $[3];
- }
- useKeybindings(t2, t3);
- let t4;
- if ($[4] !== agent.status || $[5] !== onBack || $[6] !== onDone || $[7] !== onKillAgent) {
- t4 = e => {
- if (e.key === " ") {
- e.preventDefault();
- onDone();
- } else {
- if (e.key === "left" && onBack) {
- e.preventDefault();
- onBack();
- } else {
- if (e.key === "x" && agent.status === "running" && onKillAgent) {
- e.preventDefault();
- onKillAgent();
- }
- }
- }
- };
- $[4] = agent.status;
- $[5] = onBack;
- $[6] = onDone;
- $[7] = onKillAgent;
- $[8] = t4;
- } else {
- t4 = $[8];
- }
- const handleKeyDown = t4;
- let t5;
- if ($[9] !== agent.prompt) {
- t5 = extractTag(agent.prompt, "plan");
- $[9] = agent.prompt;
- $[10] = t5;
- } else {
- t5 = $[10];
- }
- const planContent = t5;
- const displayPrompt = agent.prompt.length > 300 ? agent.prompt.substring(0, 297) + "\u2026" : agent.prompt;
- const tokenCount = agent.result?.totalTokens ?? agent.progress?.tokenCount;
- const toolUseCount = agent.result?.totalToolUseCount ?? agent.progress?.toolUseCount;
- const t6 = agent.selectedAgent?.agentType ?? "agent";
- const t7 = agent.description || "Async agent";
- let t8;
- if ($[11] !== t6 || $[12] !== t7) {
- t8 = {t6} ›{" "}{t7};
- $[11] = t6;
- $[12] = t7;
- $[13] = t8;
- } else {
- t8 = $[13];
- }
- const title = t8;
- let t9;
- if ($[14] !== agent.status) {
- t9 = agent.status !== "running" && {getTaskStatusIcon(agent.status)}{" "}{agent.status === "completed" ? "Completed" : agent.status === "failed" ? "Failed" : "Stopped"}{" \xB7 "};
- $[14] = agent.status;
- $[15] = t9;
- } else {
- t9 = $[15];
- }
- let t10;
- if ($[16] !== tokenCount) {
- t10 = tokenCount !== undefined && tokenCount > 0 && <> · {formatNumber(tokenCount)} tokens>;
- $[16] = tokenCount;
- $[17] = t10;
- } else {
- t10 = $[17];
- }
- let t11;
- if ($[18] !== toolUseCount) {
- t11 = toolUseCount !== undefined && toolUseCount > 0 && <>{" "}· {toolUseCount} {toolUseCount === 1 ? "tool" : "tools"}>;
- $[18] = toolUseCount;
- $[19] = t11;
- } else {
- t11 = $[19];
- }
- let t12;
- if ($[20] !== elapsedTime || $[21] !== t10 || $[22] !== t11) {
- t12 = {elapsedTime}{t10}{t11};
- $[20] = elapsedTime;
- $[21] = t10;
- $[22] = t11;
- $[23] = t12;
- } else {
- t12 = $[23];
- }
- let t13;
- if ($[24] !== t12 || $[25] !== t9) {
- t13 = {t9}{t12};
- $[24] = t12;
- $[25] = t9;
- $[26] = t13;
- } else {
- t13 = $[26];
- }
- const subtitle = t13;
- let t14;
- if ($[27] !== agent.status || $[28] !== onBack || $[29] !== onKillAgent) {
- t14 = exitState => exitState.pending ? Press {exitState.keyName} again to exit : {onBack && }{agent.status === "running" && onKillAgent && };
- $[27] = agent.status;
- $[28] = onBack;
- $[29] = onKillAgent;
- $[30] = t14;
- } else {
- t14 = $[30];
- }
- let t15;
- if ($[31] !== agent.progress || $[32] !== agent.status || $[33] !== theme) {
- t15 = agent.status === "running" && agent.progress?.recentActivities && agent.progress.recentActivities.length > 0 && Progress{agent.progress.recentActivities.map((activity, i) => {i === agent.progress.recentActivities.length - 1 ? "\u203A " : " "}{renderToolActivity(activity, tools, theme)})};
- $[31] = agent.progress;
- $[32] = agent.status;
- $[33] = theme;
- $[34] = t15;
- } else {
- t15 = $[34];
- }
- let t16;
- if ($[35] !== displayPrompt || $[36] !== planContent) {
- t16 = planContent ? : Prompt{displayPrompt};
- $[35] = displayPrompt;
- $[36] = planContent;
- $[37] = t16;
- } else {
- t16 = $[37];
- }
- let t17;
- if ($[38] !== agent.error || $[39] !== agent.status) {
- t17 = agent.status === "failed" && agent.error && Error{agent.error};
- $[38] = agent.error;
- $[39] = agent.status;
- $[40] = t17;
- } else {
- t17 = $[40];
- }
- let t18;
- if ($[41] !== t15 || $[42] !== t16 || $[43] !== t17) {
- t18 = {t15}{t16}{t17};
- $[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 = {t18};
- $[45] = onDone;
- $[46] = subtitle;
- $[47] = t14;
- $[48] = t18;
- $[49] = title;
- $[50] = t19;
- } else {
- t19 = $[50];
- }
- let t20;
- if ($[51] !== handleKeyDown || $[52] !== t19) {
- t20 = {t19};
- $[51] = handleKeyDown;
- $[52] = t19;
- $[53] = t20;
- } else {
- t20 = $[53];
- }
- return t20;
+ agent: DeepImmutable
+ onDone: () => void
+ onKillAgent?: () => void
+ onBack?: () => void
+}
+
+export function AsyncAgentDetailDialog({
+ agent,
+ onDone,
+ onKillAgent,
+ onBack,
+}: Props): React.ReactNode {
+ const [theme] = useTheme()
+
+ // Get tools for rendering activity messages
+ const tools = useMemo(() => getTools(getEmptyToolPermissionContext()), [])
+
+ const elapsedTime = useElapsedTime(
+ agent.startTime,
+ agent.status === 'running',
+ 1000,
+ agent.totalPausedMs ?? 0,
+ )
+
+ // Restore confirm:yes (Enter/y) dismissal — Dialog handles confirm:no (Esc)
+ // internally but does NOT auto-wire confirm:yes.
+ useKeybindings(
+ {
+ 'confirm:yes': onDone,
+ },
+ { context: 'Confirmation' },
+ )
+
+ // Component-specific shortcuts shown in UI hints (x=stop) and
+ // navigation keys (space=dismiss, left=back). These are context-dependent
+ // actions tied to agent state, not standard dialog keybindings.
+ // Note: Dialog component already handles ESC via confirm:no keybinding;
+ // confirm:yes (Enter/y) is handled by useKeybindings above.
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === ' ') {
+ e.preventDefault()
+ onDone()
+ } else if (e.key === 'left' && onBack) {
+ e.preventDefault()
+ onBack()
+ } else if (e.key === 'x' && agent.status === 'running' && onKillAgent) {
+ e.preventDefault()
+ onKillAgent()
+ }
+ }
+
+ // Extract plan from prompt - if present, we show the plan instead of the prompt
+ const planContent = extractTag(agent.prompt, 'plan')
+
+ const displayPrompt =
+ agent.prompt.length > 300
+ ? agent.prompt.substring(0, 297) + '…'
+ : agent.prompt
+
+ // Get tokens and tool uses (from result if completed, otherwise from progress)
+ const tokenCount = agent.result?.totalTokens ?? agent.progress?.tokenCount
+ const toolUseCount =
+ agent.result?.totalToolUseCount ?? agent.progress?.toolUseCount
+
+ const title = (
+
+ {agent.selectedAgent?.agentType ?? 'agent'} ›{' '}
+ {agent.description || 'Async agent'}
+
+ )
+
+ // Build subtitle with status and stats
+ const subtitle = (
+
+ {agent.status !== 'running' && (
+
+ {getTaskStatusIcon(agent.status)}{' '}
+ {agent.status === 'completed'
+ ? 'Completed'
+ : agent.status === 'failed'
+ ? 'Failed'
+ : 'Stopped'}
+ {' · '}
+
+ )}
+
+ {elapsedTime}
+ {tokenCount !== undefined && tokenCount > 0 && (
+ <> · {formatNumber(tokenCount)} tokens>
+ )}
+ {toolUseCount !== undefined && toolUseCount > 0 && (
+ <>
+ {' '}
+ · {toolUseCount} {toolUseCount === 1 ? 'tool' : 'tools'}
+ >
+ )}
+
+
+ )
+
+ return (
+
+
+ exitState.pending ? (
+ Press {exitState.keyName} again to exit
+ ) : (
+
+ {onBack && }
+
+ {agent.status === 'running' && onKillAgent && (
+
+ )}
+
+ )
+ }
+ >
+
+ {/* Recent activities for running agents */}
+ {agent.status === 'running' &&
+ agent.progress?.recentActivities &&
+ agent.progress.recentActivities.length > 0 && (
+
+
+ Progress
+
+ {agent.progress.recentActivities.map((activity, i) => (
+
+ {i === agent.progress!.recentActivities!.length - 1
+ ? '› '
+ : ' '}
+ {renderToolActivity(activity, tools, theme)}
+
+ ))}
+
+ )}
+
+ {/* Plan section (if present) - shown instead of prompt */}
+ {planContent ? (
+
+
+
+ ) : (
+ /* Prompt section - only shown when no plan */
+
+
+ Prompt
+
+ {displayPrompt}
+
+ )}
+
+ {/* Error details if failed */}
+ {agent.status === 'failed' && agent.error && (
+
+
+ Error
+
+
+ {agent.error}
+
+
+ )}
+
+
+
+ )
}
diff --git a/src/components/tasks/BackgroundTask.tsx b/src/components/tasks/BackgroundTask.tsx
index a6923b1da..fd48d09e7 100644
--- a/src/components/tasks/BackgroundTask.tsx
+++ b/src/components/tasks/BackgroundTask.tsx
@@ -1,344 +1,146 @@
-import { c as _c } from "react/compiler-runtime";
-import * as React from 'react';
-import { Text } from 'src/ink.js';
-import type { BackgroundTaskState } from 'src/tasks/types.js';
-import type { DeepImmutable } from 'src/types/utils.js';
-import { truncate } from 'src/utils/format.js';
-import { toInkColor } from 'src/utils/ink.js';
-import { plural } from 'src/utils/stringUtils.js';
-import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js';
-import { RemoteSessionProgress } from './RemoteSessionProgress.js';
-import { ShellProgress, TaskStatusText } from './ShellProgress.js';
-import { describeTeammateActivity } from './taskStatusUtils.js';
+import * as React from 'react'
+import { Text } from 'src/ink.js'
+import type { BackgroundTaskState } from 'src/tasks/types.js'
+import type { DeepImmutable } from 'src/types/utils.js'
+import { truncate } from 'src/utils/format.js'
+import { toInkColor } from 'src/utils/ink.js'
+import { plural } from 'src/utils/stringUtils.js'
+import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'
+import { RemoteSessionProgress } from './RemoteSessionProgress.js'
+import { ShellProgress, TaskStatusText } from './ShellProgress.js'
+import { describeTeammateActivity } from './taskStatusUtils.js'
+
type Props = {
- task: DeepImmutable;
- maxActivityWidth?: number;
-};
-export function BackgroundTask(t0) {
- const $ = _c(92);
- const {
- task,
- maxActivityWidth
- } = t0;
- const activityLimit = maxActivityWidth ?? 40;
+ task: DeepImmutable
+ maxActivityWidth?: number
+}
+
+export function BackgroundTask({
+ task,
+ maxActivityWidth,
+}: Props): React.ReactNode {
+ const activityLimit = maxActivityWidth ?? 40
switch (task.type) {
- case "local_bash":
- {
- const t1 = task.kind === "monitor" ? task.description : task.command;
- let t2;
- if ($[0] !== activityLimit || $[1] !== t1) {
- t2 = truncate(t1, activityLimit, true);
- $[0] = activityLimit;
- $[1] = t1;
- $[2] = t2;
- } else {
- t2 = $[2];
- }
- let t3;
- if ($[3] !== task) {
- t3 = ;
- $[3] = task;
- $[4] = t3;
- } else {
- t3 = $[4];
- }
- let t4;
- if ($[5] !== t2 || $[6] !== t3) {
- t4 = {t2}{" "}{t3};
- $[5] = t2;
- $[6] = t3;
- $[7] = t4;
- } else {
- t4 = $[7];
- }
- return t4;
- }
- case "remote_agent":
- {
- if (task.isRemoteReview) {
- let t1;
- if ($[8] !== task) {
- t1 = ;
- $[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 = {t1} ;
- $[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 = · ;
- $[15] = t4;
- } else {
- t4 = $[15];
- }
- let t5;
- if ($[16] !== task) {
- t5 = ;
- $[16] = task;
- $[17] = t5;
- } else {
- t5 = $[17];
- }
- let t6;
- if ($[18] !== t2 || $[19] !== t3 || $[20] !== t5) {
- t6 = {t2}{t3}{t4}{t5};
- $[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 = ;
- $[25] = t2;
- $[26] = t3;
- $[27] = task.status;
- $[28] = t4;
- } else {
- t4 = $[28];
- }
- let t5;
- if ($[29] !== t1 || $[30] !== t4) {
- t5 = {t1}{" "}{t4};
- $[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 = @{task.identity.agentName};
- $[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 = {t2}{t3};
- $[45] = T0;
- $[46] = t1;
- $[47] = t2;
- $[48] = t3;
- $[49] = t5;
- } else {
- t5 = $[49];
- }
- let t6;
- if ($[50] !== T1 || $[51] !== t4 || $[52] !== t5) {
- t6 = {t4}{t5};
- $[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 = ;
- $[60] = t3;
- $[61] = t4;
- $[62] = task.status;
- $[63] = t5;
- } else {
- t5 = $[63];
- }
- let t6;
- if ($[64] !== t2 || $[65] !== t5) {
- t6 = {t2}{" "}{t5};
- $[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 = ;
- $[70] = t2;
- $[71] = t3;
- $[72] = task.status;
- $[73] = t4;
- } else {
- t4 = $[73];
- }
- let t5;
- if ($[74] !== t1 || $[75] !== t4) {
- t5 = {t1}{" "}{t4};
- $[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 = · {task.phase} · {detail};
- $[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 = ;
- $[84] = t3;
- $[85] = t4;
- $[86] = task.status;
- $[87] = t5;
- } else {
- t5 = $[87];
- }
- let t6;
- if ($[88] !== t2 || $[89] !== t5 || $[90] !== task.description) {
- t6 = {task.description}{" "}{t2}{" "}{t5};
- $[88] = t2;
- $[89] = t5;
- $[90] = task.description;
- $[91] = t6;
- } else {
- t6 = $[91];
- }
- return t6;
+ case 'local_bash':
+ return (
+
+ {truncate(
+ task.kind === 'monitor' ? task.description : task.command,
+ activityLimit,
+ true,
+ )}{' '}
+
+
+ )
+ case 'remote_agent': {
+ // Lite-review renders its own rainbow line (title + live counts),
+ // so we don't prefix the title — the rainbow already includes it.
+ if (task.isRemoteReview) {
+ return (
+
+
+
+ )
}
+ const running = task.status === 'running' || task.status === 'pending'
+ return (
+
+ {running ? DIAMOND_OPEN : DIAMOND_FILLED}
+ {truncate(task.title, activityLimit, true)}
+ ·
+
+
+ )
+ }
+ case 'local_agent':
+ return (
+
+ {truncate(task.description, activityLimit, true)}{' '}
+
+
+ )
+ case 'in_process_teammate': {
+ const activity = describeTeammateActivity(task)
+ return (
+
+
+ @{task.identity.agentName}
+
+ : {truncate(activity, activityLimit, true)}
+
+ )
+ }
+ case 'local_workflow':
+ return (
+
+ {truncate(
+ task.workflowName ?? task.summary ?? task.description,
+ activityLimit,
+ true,
+ )}{' '}
+
+
+ )
+ case 'monitor_mcp':
+ return (
+
+ {truncate(task.description, activityLimit, true)}{' '}
+
+
+ )
+ 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 (
+
+ {task.description}{' '}
+
+ · {task.phase} · {detail}
+ {' '}
+
+
+ )
+ }
}
}
diff --git a/src/components/tasks/BackgroundTaskStatus.tsx b/src/components/tasks/BackgroundTaskStatus.tsx
index 37bfd8009..26d46cf98 100644
--- a/src/components/tasks/BackgroundTaskStatus.tsx
+++ b/src/components/tasks/BackgroundTaskStatus.tsx
@@ -1,428 +1,310 @@
-import { c as _c } from "react/compiler-runtime";
-import figures from 'figures';
-import * as React from 'react';
-import { useMemo, useState } from 'react';
-import { useTerminalSize } from 'src/hooks/useTerminalSize.js';
-import { stringWidth } from 'src/ink/stringWidth.js';
-import { useAppState, useSetAppState } from 'src/state/AppState.js';
-import { enterTeammateView, exitTeammateView } from 'src/state/teammateViewHelpers.js';
-import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js';
-import { getPillLabel, pillNeedsCta } from 'src/tasks/pillLabel.js';
-import { type BackgroundTaskState, isBackgroundTask, type TaskState } from 'src/tasks/types.js';
-import { calculateHorizontalScrollWindow } from 'src/utils/horizontalScroll.js';
-import { Box, Text } from '../../ink.js';
-import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from '../../tools/AgentTool/agentColorManager.js';
-import type { Theme } from '../../utils/theme.js';
-import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
-import { shouldHideTasksFooter } from './taskStatusUtils.js';
+import figures from 'figures'
+import * as React from 'react'
+import { useMemo, useState } from 'react'
+import { useTerminalSize } from 'src/hooks/useTerminalSize.js'
+import { stringWidth } from 'src/ink/stringWidth.js'
+import { useAppState, useSetAppState } from 'src/state/AppState.js'
+import {
+ enterTeammateView,
+ exitTeammateView,
+} from 'src/state/teammateViewHelpers.js'
+import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
+import { getPillLabel, pillNeedsCta } from 'src/tasks/pillLabel.js'
+import {
+ type BackgroundTaskState,
+ isBackgroundTask,
+ type TaskState,
+} from 'src/tasks/types.js'
+import { calculateHorizontalScrollWindow } from 'src/utils/horizontalScroll.js'
+import { Box, Text } from '../../ink.js'
+import {
+ AGENT_COLOR_TO_THEME_COLOR,
+ AGENT_COLORS,
+ type AgentColorName,
+} from '../../tools/AgentTool/agentColorManager.js'
+import type { Theme } from '../../utils/theme.js'
+import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
+import { shouldHideTasksFooter } from './taskStatusUtils.js'
+
type Props = {
- tasksSelected: boolean;
- isViewingTeammate?: boolean;
- teammateFooterIndex?: number;
- isLeaderIdle?: boolean;
- onOpenDialog?: (taskId?: string) => void;
-};
-export function BackgroundTaskStatus(t0) {
- const $ = _c(48);
- const {
- tasksSelected,
- isViewingTeammate,
- teammateFooterIndex: t1,
- isLeaderIdle: t2,
- onOpenDialog
- } = t0;
- const teammateFooterIndex = t1 === undefined ? 0 : t1;
- const isLeaderIdle = t2 === undefined ? false : t2;
- const setAppState = useSetAppState();
- const {
- columns
- } = useTerminalSize();
- const tasks = useAppState(_temp);
- const viewingAgentTaskId = useAppState(_temp2);
- let t3;
- if ($[0] !== tasks) {
- t3 = (Object.values(tasks ?? {}) as TaskState[]).filter(_temp3);
- $[0] = tasks;
- $[1] = t3;
- } else {
- t3 = $[1];
- }
- const runningTasks = t3;
- const expandedView = useAppState(_temp4);
- const showSpinnerTree = expandedView === "teammates";
- const allTeammates = !showSpinnerTree && runningTasks.length > 0 && runningTasks.every(_temp5);
- let t4;
- if ($[2] !== runningTasks) {
- t4 = runningTasks.filter(_temp6).sort(_temp7);
- $[2] = runningTasks;
- $[3] = t4;
- } else {
- t4 = $[3];
- }
- const teammateEntries = t4;
- let t5;
- if ($[4] !== isLeaderIdle) {
- t5 = {
- name: "main",
+ tasksSelected: boolean
+ isViewingTeammate?: boolean
+ teammateFooterIndex?: number
+ isLeaderIdle?: boolean
+ onOpenDialog?: (taskId?: string) => void
+}
+
+export function BackgroundTaskStatus({
+ tasksSelected,
+ isViewingTeammate,
+ teammateFooterIndex = 0,
+ isLeaderIdle = false,
+ onOpenDialog,
+}: Props): React.ReactNode {
+ const setAppState = useSetAppState()
+ const { columns } = useTerminalSize()
+ const tasks = useAppState(s => s.tasks)
+ const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId)
+
+ const runningTasks = useMemo(
+ () =>
+ (Object.values(tasks ?? {}) as TaskState[]).filter(
+ t =>
+ isBackgroundTask(t) &&
+ !(process.env.USER_TYPE === 'ant' && isPanelAgentTask(t)),
+ ),
+ [tasks],
+ )
+
+ // Check if all tasks are in-process teammates (team mode)
+ // In spinner-tree mode, don't show teammate pills (teammates appear in the spinner tree)
+ const expandedView = useAppState(s => s.expandedView)
+ const showSpinnerTree = expandedView === 'teammates'
+ const allTeammates =
+ !showSpinnerTree &&
+ runningTasks.length > 0 &&
+ runningTasks.every(t => t.type === 'in_process_teammate')
+
+ // Memoize teammate-related computations at the top level (rules of hooks)
+ const teammateEntries = useMemo(
+ () =>
+ runningTasks
+ .filter(
+ (t): t is BackgroundTaskState & { type: 'in_process_teammate' } =>
+ t.type === 'in_process_teammate',
+ )
+ .sort((a, b) =>
+ a.identity.agentName.localeCompare(b.identity.agentName),
+ ),
+ [runningTasks],
+ )
+
+ // Build array of all pills with their activity state
+ // Each pill is "@{name}" and separator is " " (1 char)
+ // Sort idle agents to the end, but only when not in selection mode
+ // to avoid reordering while user is arrowing through the list
+ // "main" always stays first regardless of idle state
+ const allPills = useMemo(() => {
+ const mainPill = {
+ name: 'main',
color: undefined as keyof Theme | undefined,
isIdle: isLeaderIdle,
- taskId: undefined as string | undefined
- };
- $[4] = isLeaderIdle;
- $[5] = t5;
- } else {
- t5 = $[5];
- }
- const mainPill = t5;
- let t6;
- if ($[6] !== mainPill || $[7] !== tasksSelected || $[8] !== teammateEntries) {
- const teammatePills = teammateEntries.map(_temp8);
+ taskId: undefined as string | undefined,
+ }
+
+ const teammatePills = teammateEntries.map(t => ({
+ name: t.identity.agentName,
+ color: getAgentThemeColor(t.identity.color),
+ isIdle: t.isIdle,
+ taskId: t.id,
+ }))
+
+ // Only sort teammates when not selecting to avoid reordering during navigation
if (!tasksSelected) {
- teammatePills.sort(_temp9);
+ teammatePills.sort((a, b) => {
+ // Active agents first, idle agents last
+ if (a.isIdle !== b.isIdle) return a.isIdle ? 1 : -1
+ return 0 // Keep original order within each group
+ })
}
- const pills = [mainPill, ...teammatePills];
- t6 = pills.map(_temp0);
- $[6] = mainPill;
- $[7] = tasksSelected;
- $[8] = teammateEntries;
- $[9] = t6;
- } else {
- t6 = $[9];
- }
- const allPills = t6;
- let t7;
- if ($[10] !== allPills) {
- t7 = allPills.map(_temp1);
- $[10] = allPills;
- $[11] = t7;
- } else {
- t7 = $[11];
- }
- const pillWidths = t7;
- if (allTeammates || !showSpinnerTree && isViewingTeammate) {
- const selectedIdx = tasksSelected ? teammateFooterIndex : -1;
- let t8;
- if ($[12] !== teammateEntries || $[13] !== viewingAgentTaskId) {
- t8 = viewingAgentTaskId ? teammateEntries.findIndex(t_3 => t_3.id === viewingAgentTaskId) + 1 : 0;
- $[12] = teammateEntries;
- $[13] = viewingAgentTaskId;
- $[14] = t8;
- } else {
- t8 = $[14];
- }
- const viewedIdx = t8;
- const availableWidth = Math.max(20, columns - 20 - 4);
- const t9 = selectedIdx >= 0 ? selectedIdx : 0;
- let t10;
- if ($[15] !== availableWidth || $[16] !== pillWidths || $[17] !== t9) {
- t10 = calculateHorizontalScrollWindow(pillWidths, availableWidth, 2, t9);
- $[15] = availableWidth;
- $[16] = pillWidths;
- $[17] = t9;
- $[18] = t10;
- } else {
- t10 = $[18];
- }
- const {
- startIndex,
- endIndex,
- showLeftArrow,
- showRightArrow
- } = t10;
- let t11;
- if ($[19] !== allPills || $[20] !== endIndex || $[21] !== startIndex) {
- t11 = allPills.slice(startIndex, endIndex);
- $[19] = allPills;
- $[20] = endIndex;
- $[21] = startIndex;
- $[22] = t11;
- } else {
- t11 = $[22];
- }
- const visiblePills = t11;
- let t12;
- if ($[23] !== showLeftArrow) {
- t12 = showLeftArrow && {figures.arrowLeft} ;
- $[23] = showLeftArrow;
- $[24] = t12;
- } else {
- t12 = $[24];
- }
- let t13;
- if ($[25] !== selectedIdx || $[26] !== setAppState || $[27] !== viewedIdx || $[28] !== visiblePills) {
- t13 = visiblePills.map((pill_1, i_1) => {
- const needsSeparator = i_1 > 0;
- return {needsSeparator && } pill_1.taskId ? enterTeammateView(pill_1.taskId, setAppState) : exitTeammateView(setAppState)} />;
- });
- $[25] = selectedIdx;
- $[26] = setAppState;
- $[27] = viewedIdx;
- $[28] = visiblePills;
- $[29] = t13;
- } else {
- t13 = $[29];
- }
- let t14;
- if ($[30] !== showRightArrow) {
- t14 = showRightArrow && {figures.arrowRight};
- $[30] = showRightArrow;
- $[31] = t14;
- } else {
- t14 = $[31];
- }
- let t15;
- if ($[32] === Symbol.for("react.memo_cache_sentinel")) {
- t15 = {" \xB7 "};
- $[32] = t15;
- } else {
- t15 = $[32];
- }
- let t16;
- if ($[33] !== t12 || $[34] !== t13 || $[35] !== t14) {
- t16 = <>{t12}{t13}{t14}{t15}>;
- $[33] = t12;
- $[34] = t13;
- $[35] = t14;
- $[36] = t16;
- } else {
- t16 = $[36];
- }
- return t16;
+
+ // main always first, then sorted teammates
+ const pills = [mainPill, ...teammatePills]
+
+ // Add idx after sorting
+ return pills.map((pill, i) => ({ ...pill, idx: i }))
+ }, [teammateEntries, isLeaderIdle, tasksSelected])
+
+ // Calculate pill widths (including separator space, except first)
+ const pillWidths = useMemo(
+ () =>
+ allPills.map((pill, i) => {
+ const pillText = `@${pill.name}`
+ // First pill has no leading space, others have 1 space separator
+ return stringWidth(pillText) + (i > 0 ? 1 : 0)
+ }),
+ [allPills],
+ )
+
+ if (allTeammates || (!showSpinnerTree && isViewingTeammate)) {
+ const selectedIdx = tasksSelected ? teammateFooterIndex : -1
+ // Which agent is currently foregrounded (bold)
+ const viewedIdx = viewingAgentTaskId
+ ? teammateEntries.findIndex(t => t.id === viewingAgentTaskId) + 1
+ : 0 // 0 = main/leader
+
+ // Calculate available width for pills
+ // Reserve space for: arrows, hint, and minimal padding
+ // Pills are rendered on their own line when in team mode
+ const ARROW_WIDTH = 2 // arrow char + space
+ const HINT_WIDTH = 20 // shift+↓ to expand
+ const PADDING = 4 // minimal safety margin
+ const availableWidth = Math.max(20, columns - HINT_WIDTH - PADDING)
+
+ // Calculate visible window of pills
+ const { startIndex, endIndex, showLeftArrow, showRightArrow } =
+ calculateHorizontalScrollWindow(
+ pillWidths,
+ availableWidth,
+ ARROW_WIDTH,
+ selectedIdx >= 0 ? selectedIdx : 0,
+ )
+
+ const visiblePills = allPills.slice(startIndex, endIndex)
+
+ return (
+ <>
+ {showLeftArrow && {figures.arrowLeft} }
+ {visiblePills.map((pill, i) => {
+ // First visible pill has no leading separator
+ // (left arrow already provides spacing if present)
+ const needsSeparator = i > 0
+ return (
+
+ {needsSeparator && }
+
+ pill.taskId
+ ? enterTeammateView(pill.taskId, setAppState)
+ : exitTeammateView(setAppState)
+ }
+ />
+
+ )
+ })}
+ {showRightArrow && {figures.arrowRight}}
+
+ {' · '}
+
+
+ >
+ )
}
+
+ // In spinner-tree mode, don't show any footer status for teammates
+ // (they appear in the spinner tree above)
if (shouldHideTasksFooter(tasks ?? {}, showSpinnerTree)) {
- return null;
+ return null
}
+
if (runningTasks.length === 0) {
- return null;
+ return null
}
- let t8;
- if ($[37] !== runningTasks) {
- t8 = getPillLabel(runningTasks);
- $[37] = runningTasks;
- $[38] = t8;
- } else {
- t8 = $[38];
- }
- let t9;
- if ($[39] !== onOpenDialog || $[40] !== t8 || $[41] !== tasksSelected) {
- t9 = {t8};
- $[39] = onOpenDialog;
- $[40] = t8;
- $[41] = tasksSelected;
- $[42] = t9;
- } else {
- t9 = $[42];
- }
- let t10;
- if ($[43] !== runningTasks) {
- t10 = pillNeedsCta(runningTasks) && · {figures.arrowDown} to view;
- $[43] = runningTasks;
- $[44] = t10;
- } else {
- t10 = $[44];
- }
- let t11;
- if ($[45] !== t10 || $[46] !== t9) {
- t11 = <>{t9}{t10}>;
- $[45] = t10;
- $[46] = t9;
- $[47] = t11;
- } else {
- t11 = $[47];
- }
- return t11;
-}
-function _temp1(pill_0, i_0) {
- const pillText = `@${pill_0.name}`;
- return stringWidth(pillText) + (i_0 > 0 ? 1 : 0);
-}
-function _temp0(pill, i) {
- return {
- ...pill,
- idx: i
- };
-}
-function _temp9(a_0, b_0) {
- if (a_0.isIdle !== b_0.isIdle) {
- return a_0.isIdle ? 1 : -1;
- }
- return 0;
-}
-function _temp8(t_2) {
- return {
- name: t_2.identity.agentName,
- color: getAgentThemeColor(t_2.identity.color),
- isIdle: t_2.isIdle,
- taskId: t_2.id
- };
-}
-function _temp7(a, b) {
- return a.identity.agentName.localeCompare(b.identity.agentName);
-}
-function _temp6(t_1) {
- return t_1.type === "in_process_teammate";
-}
-function _temp5(t_0) {
- return t_0.type === "in_process_teammate";
-}
-function _temp4(s_1) {
- return s_1.expandedView;
-}
-function _temp3(t) {
- return isBackgroundTask(t) && !(false && isPanelAgentTask(t));
-}
-function _temp2(s_0) {
- return s_0.viewingAgentTaskId;
-}
-function _temp(s) {
- return s.tasks;
+
+ return (
+ <>
+
+ {getPillLabel(runningTasks)}
+
+ {pillNeedsCta(runningTasks) && (
+ · {figures.arrowDown} to view
+ )}
+ >
+ )
}
+
type AgentPillProps = {
- name: string;
- color?: keyof Theme;
- isSelected: boolean;
- isViewed: boolean;
- isIdle: boolean;
- onClick?: () => void;
-};
-function AgentPill(t0) {
- const $ = _c(19);
- const {
- name,
- color,
- isSelected,
- isViewed,
- isIdle,
- onClick
- } = t0;
- const [hover, setHover] = useState(false);
- const highlighted = isSelected || hover;
- let label;
+ name: string
+ color?: keyof Theme
+ isSelected: boolean
+ isViewed: boolean
+ isIdle: boolean
+ onClick?: () => void
+}
+
+function AgentPill({
+ name,
+ color,
+ isSelected,
+ isViewed,
+ isIdle,
+ onClick,
+}: AgentPillProps): React.ReactNode {
+ const [hover, setHover] = useState(false)
+ // Hover mirrors the keyboard-selected look so the affordance is familiar.
+ const highlighted = isSelected || hover
+
+ let label: React.ReactNode
if (highlighted) {
- let t1;
- if ($[0] !== color || $[1] !== isViewed || $[2] !== name) {
- t1 = color ? @{name} : @{name};
- $[0] = color;
- $[1] = isViewed;
- $[2] = name;
- $[3] = t1;
- } else {
- t1 = $[3];
- }
- label = t1;
+ label = color ? (
+
+ @{name}
+
+ ) : (
+
+ @{name}
+
+ )
+ } else if (isIdle) {
+ label = (
+
+ @{name}
+
+ )
+ } else if (isViewed) {
+ label = (
+
+ @{name}
+
+ )
} else {
- if (isIdle) {
- let t1;
- if ($[4] !== isViewed || $[5] !== name) {
- t1 = @{name};
- $[4] = isViewed;
- $[5] = name;
- $[6] = t1;
- } else {
- t1 = $[6];
- }
- label = t1;
- } else {
- if (isViewed) {
- let t1;
- if ($[7] !== color || $[8] !== name) {
- t1 = @{name};
- $[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 = @{name};
- $[10] = color;
- $[11] = name;
- $[12] = t1;
- $[13] = t2;
- } else {
- t2 = $[13];
- }
- label = t2;
- }
- }
+ label = (
+
+ @{name}
+
+ )
}
- if (!onClick) {
- return label;
- }
- let t1;
- let t2;
- if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = () => setHover(true);
- t2 = () => setHover(false);
- $[14] = t1;
- $[15] = t2;
- } else {
- t1 = $[14];
- t2 = $[15];
- }
- let t3;
- if ($[16] !== label || $[17] !== onClick) {
- t3 = {label};
- $[16] = label;
- $[17] = onClick;
- $[18] = t3;
- } else {
- t3 = $[18];
- }
- return t3;
+
+ if (!onClick) return label
+ return (
+ setHover(true)}
+ onMouseLeave={() => setHover(false)}
+ >
+ {label}
+
+ )
}
-function SummaryPill(t0) {
- const $ = _c(8);
- const {
- selected,
- onClick,
- children
- } = t0;
- const [hover, setHover] = useState(false);
- const t1 = selected || hover;
- let t2;
- if ($[0] !== children || $[1] !== t1) {
- t2 = {children};
- $[0] = children;
- $[1] = t1;
- $[2] = t2;
- } else {
- t2 = $[2];
- }
- const label = t2;
- if (!onClick) {
- return label;
- }
- let t3;
- let t4;
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
- t3 = () => setHover(true);
- t4 = () => setHover(false);
- $[3] = t3;
- $[4] = t4;
- } else {
- t3 = $[3];
- t4 = $[4];
- }
- let t5;
- if ($[5] !== label || $[6] !== onClick) {
- t5 = {label};
- $[5] = label;
- $[6] = onClick;
- $[7] = t5;
- } else {
- t5 = $[7];
- }
- return t5;
+
+function SummaryPill({
+ selected,
+ onClick,
+ children,
+}: {
+ selected: boolean
+ onClick?: () => void
+ children: React.ReactNode
+}): React.ReactNode {
+ const [hover, setHover] = useState(false)
+ const label = (
+
+ {children}
+
+ )
+ if (!onClick) return label
+ return (
+ setHover(true)}
+ onMouseLeave={() => setHover(false)}
+ >
+ {label}
+
+ )
}
-function getAgentThemeColor(colorName: string | undefined): keyof Theme | undefined {
- if (!colorName) return undefined;
+
+function getAgentThemeColor(
+ colorName: string | undefined,
+): keyof Theme | undefined {
+ if (!colorName) return undefined
if (AGENT_COLORS.includes(colorName as AgentColorName)) {
- return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName];
+ return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]
}
- return undefined;
+ return undefined
}
diff --git a/src/components/tasks/BackgroundTasksDialog.tsx b/src/components/tasks/BackgroundTasksDialog.tsx
index c7bbd9b60..d9f119cf1 100644
--- a/src/components/tasks/BackgroundTasksDialog.tsx
+++ b/src/components/tasks/BackgroundTasksDialog.tsx
@@ -1,171 +1,214 @@
-import { c as _c } from "react/compiler-runtime";
-import { feature } from 'bun:bundle';
-import figures from 'figures';
-import React, { type ReactNode, useEffect, useEffectEvent, useMemo, useRef, useState } from 'react';
-import { isCoordinatorMode } from 'src/coordinator/coordinatorMode.js';
-import { useTerminalSize } from 'src/hooks/useTerminalSize.js';
-import { useAppState, useSetAppState } from 'src/state/AppState.js';
-import { enterTeammateView, exitTeammateView } from 'src/state/teammateViewHelpers.js';
-import type { ToolUseContext } from 'src/Tool.js';
-import { DreamTask, type DreamTaskState } from 'src/tasks/DreamTask/DreamTask.js';
-import { InProcessTeammateTask } from 'src/tasks/InProcessTeammateTask/InProcessTeammateTask.js';
-import type { InProcessTeammateTaskState } from 'src/tasks/InProcessTeammateTask/types.js';
-import type { LocalAgentTaskState } from 'src/tasks/LocalAgentTask/LocalAgentTask.js';
-import { LocalAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js';
-import type { LocalShellTaskState } from 'src/tasks/LocalShellTask/guards.js';
-import { LocalShellTask } from 'src/tasks/LocalShellTask/LocalShellTask.js';
+import { feature } from 'bun:bundle'
+import figures from 'figures'
+import React, {
+ type ReactNode,
+ useEffect,
+ useEffectEvent,
+ useMemo,
+ useRef,
+ useState,
+} from 'react'
+import { isCoordinatorMode } from 'src/coordinator/coordinatorMode.js'
+import { useTerminalSize } from 'src/hooks/useTerminalSize.js'
+import { useAppState, useSetAppState } from 'src/state/AppState.js'
+import {
+ enterTeammateView,
+ exitTeammateView,
+} from 'src/state/teammateViewHelpers.js'
+import type { ToolUseContext } from 'src/Tool.js'
+import {
+ DreamTask,
+ type DreamTaskState,
+} from 'src/tasks/DreamTask/DreamTask.js'
+import { InProcessTeammateTask } from 'src/tasks/InProcessTeammateTask/InProcessTeammateTask.js'
+import type { InProcessTeammateTaskState } from 'src/tasks/InProcessTeammateTask/types.js'
+import type { LocalAgentTaskState } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
+import { LocalAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
+import type { LocalShellTaskState } from 'src/tasks/LocalShellTask/guards.js'
+import { LocalShellTask } from 'src/tasks/LocalShellTask/LocalShellTask.js'
// Type import is erased at build time — safe even though module is ant-gated.
-import type { LocalWorkflowTaskState } from 'src/tasks/LocalWorkflowTask/LocalWorkflowTask.js';
-import type { MonitorMcpTaskState } from 'src/tasks/MonitorMcpTask/MonitorMcpTask.js';
-import { RemoteAgentTask, type RemoteAgentTaskState } from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js';
-import { type BackgroundTaskState, isBackgroundTask, type TaskState } from 'src/tasks/types.js';
-import type { DeepImmutable } from 'src/types/utils.js';
-import { intersperse } from 'src/utils/array.js';
-import { TEAM_LEAD_NAME } from 'src/utils/swarm/constants.js';
-import { stopUltraplan } from '../../commands/ultraplan.js';
-import type { CommandResultDisplay } from '../../commands.js';
-import { useRegisterOverlay } from '../../context/overlayContext.js';
-import type { ExitState } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
-import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
-import { Box, Text } from '../../ink.js';
-import { useKeybindings } from '../../keybindings/useKeybinding.js';
-import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
-import { count } from '../../utils/array.js';
-import { Byline } from '../design-system/Byline.js';
-import { Dialog } from '../design-system/Dialog.js';
-import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
-import { AsyncAgentDetailDialog } from './AsyncAgentDetailDialog.js';
-import { BackgroundTask as BackgroundTaskComponent } from './BackgroundTask.js';
-import { DreamDetailDialog } from './DreamDetailDialog.js';
-import { InProcessTeammateDetailDialog } from './InProcessTeammateDetailDialog.js';
-import { RemoteSessionDetailDialog } from './RemoteSessionDetailDialog.js';
-import { ShellDetailDialog } from './ShellDetailDialog.js';
-type ViewState = {
- mode: 'list';
-} | {
- mode: 'detail';
- itemId: string;
-};
+import type { LocalWorkflowTaskState } from 'src/tasks/LocalWorkflowTask/LocalWorkflowTask.js'
+import type { MonitorMcpTaskState } from 'src/tasks/MonitorMcpTask/MonitorMcpTask.js'
+import {
+ RemoteAgentTask,
+ type RemoteAgentTaskState,
+} from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js'
+import {
+ type BackgroundTaskState,
+ isBackgroundTask,
+ type TaskState,
+} from 'src/tasks/types.js'
+import type { DeepImmutable } from 'src/types/utils.js'
+import { intersperse } from 'src/utils/array.js'
+import { TEAM_LEAD_NAME } from 'src/utils/swarm/constants.js'
+import { stopUltraplan } from '../../commands/ultraplan.js'
+import type { CommandResultDisplay } from '../../commands.js'
+import { useRegisterOverlay } from '../../context/overlayContext.js'
+import type { ExitState } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
+import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
+import { Box, Text } from '../../ink.js'
+import { useKeybindings } from '../../keybindings/useKeybinding.js'
+import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
+import { count } from '../../utils/array.js'
+import { Byline } from '../design-system/Byline.js'
+import { Dialog } from '../design-system/Dialog.js'
+import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
+import { AsyncAgentDetailDialog } from './AsyncAgentDetailDialog.js'
+import { BackgroundTask as BackgroundTaskComponent } from './BackgroundTask.js'
+import { DreamDetailDialog } from './DreamDetailDialog.js'
+import { InProcessTeammateDetailDialog } from './InProcessTeammateDetailDialog.js'
+import { RemoteSessionDetailDialog } from './RemoteSessionDetailDialog.js'
+import { ShellDetailDialog } from './ShellDetailDialog.js'
+
+type ViewState = { mode: 'list' } | { mode: 'detail'; itemId: string }
+
type Props = {
- onDone: (result?: string, options?: {
- display?: CommandResultDisplay;
- }) => void;
- toolUseContext: ToolUseContext;
- initialDetailTaskId?: string;
-};
-type ListItem = {
- id: string;
- type: 'local_bash';
- label: string;
- status: string;
- task: DeepImmutable;
-} | {
- id: string;
- type: 'remote_agent';
- label: string;
- status: string;
- task: DeepImmutable;
-} | {
- id: string;
- type: 'local_agent';
- label: string;
- status: string;
- task: DeepImmutable;
-} | {
- id: string;
- type: 'in_process_teammate';
- label: string;
- status: string;
- task: DeepImmutable;
-} | {
- id: string;
- type: 'local_workflow';
- label: string;
- status: string;
- task: DeepImmutable;
-} | {
- id: string;
- type: 'monitor_mcp';
- label: string;
- status: string;
- task: DeepImmutable;
-} | {
- id: string;
- type: 'dream';
- label: string;
- status: string;
- task: DeepImmutable;
-} | {
- id: string;
- type: 'leader';
- label: string;
- status: 'running';
-};
+ onDone: (
+ result?: string,
+ options?: { display?: CommandResultDisplay },
+ ) => void
+ toolUseContext: ToolUseContext
+ initialDetailTaskId?: string
+}
+
+type ListItem =
+ | {
+ id: string
+ type: 'local_bash'
+ label: string
+ status: string
+ task: DeepImmutable
+ }
+ | {
+ id: string
+ type: 'remote_agent'
+ label: string
+ status: string
+ task: DeepImmutable
+ }
+ | {
+ id: string
+ type: 'local_agent'
+ label: string
+ status: string
+ task: DeepImmutable
+ }
+ | {
+ id: string
+ type: 'in_process_teammate'
+ label: string
+ status: string
+ task: DeepImmutable
+ }
+ | {
+ id: string
+ type: 'local_workflow'
+ label: string
+ status: string
+ task: DeepImmutable
+ }
+ | {
+ id: string
+ type: 'monitor_mcp'
+ label: string
+ status: string
+ task: DeepImmutable
+ }
+ | {
+ id: string
+ type: 'dream'
+ label: string
+ status: string
+ task: DeepImmutable
+ }
+ | {
+ id: string
+ type: 'leader'
+ label: string
+ status: 'running'
+ }
// WORKFLOW_SCRIPTS is ant-only (build_flags.yaml). Static imports would leak
// ~1.3K lines into external builds. Gate with feature() + require so the
// bundler can dead-code-eliminate the branch.
/* eslint-disable @typescript-eslint/no-require-imports */
-const WorkflowDetailDialog = feature('WORKFLOW_SCRIPTS') ? (require('./WorkflowDetailDialog.js') as typeof import('./WorkflowDetailDialog.js')).WorkflowDetailDialog : null;
-const workflowTaskModule = feature('WORKFLOW_SCRIPTS') ? require('src/tasks/LocalWorkflowTask/LocalWorkflowTask.js') as typeof import('src/tasks/LocalWorkflowTask/LocalWorkflowTask.js') : null;
-const killWorkflowTask = workflowTaskModule?.killWorkflowTask ?? null;
-const skipWorkflowAgent = workflowTaskModule?.skipWorkflowAgent ?? null;
-const retryWorkflowAgent = workflowTaskModule?.retryWorkflowAgent ?? null;
+const WorkflowDetailDialog = feature('WORKFLOW_SCRIPTS')
+ ? (
+ require('./WorkflowDetailDialog.js') as typeof import('./WorkflowDetailDialog.js')
+ ).WorkflowDetailDialog
+ : null
+const workflowTaskModule = feature('WORKFLOW_SCRIPTS')
+ ? (require('src/tasks/LocalWorkflowTask/LocalWorkflowTask.js') as typeof import('src/tasks/LocalWorkflowTask/LocalWorkflowTask.js'))
+ : null
+const killWorkflowTask = workflowTaskModule?.killWorkflowTask ?? null
+const skipWorkflowAgent = workflowTaskModule?.skipWorkflowAgent ?? null
+const retryWorkflowAgent = workflowTaskModule?.retryWorkflowAgent ?? null
// Relative path, not `src/...` path-mapping — Bun's DCE can statically
// resolve + eliminate `./` requires, but path-mapped strings stay opaque
// and survive as dead literals in the bundle. Matches tasks.ts pattern.
-const monitorMcpModule = feature('MONITOR_TOOL') ? require('../../tasks/MonitorMcpTask/MonitorMcpTask.js') as typeof import('../../tasks/MonitorMcpTask/MonitorMcpTask.js') : null;
-const killMonitorMcp = monitorMcpModule?.killMonitorMcp ?? null;
-const MonitorMcpDetailDialog = feature('MONITOR_TOOL') ? (require('./MonitorMcpDetailDialog.js') as typeof import('./MonitorMcpDetailDialog.js')).MonitorMcpDetailDialog : null;
+const monitorMcpModule = feature('MONITOR_TOOL')
+ ? (require('../../tasks/MonitorMcpTask/MonitorMcpTask.js') as typeof import('../../tasks/MonitorMcpTask/MonitorMcpTask.js'))
+ : null
+const killMonitorMcp = monitorMcpModule?.killMonitorMcp ?? null
+const MonitorMcpDetailDialog = feature('MONITOR_TOOL')
+ ? (
+ require('./MonitorMcpDetailDialog.js') as typeof import('./MonitorMcpDetailDialog.js')
+ ).MonitorMcpDetailDialog
+ : null
/* eslint-enable @typescript-eslint/no-require-imports */
// Helper to get filtered background tasks (excludes foregrounded local_agent)
-function getSelectableBackgroundTasks(tasks: Record | undefined, foregroundedTaskId: string | undefined): TaskState[] {
- const backgroundTasks = Object.values(tasks ?? {}).filter(isBackgroundTask);
- return backgroundTasks.filter(task => !(task.type === 'local_agent' && task.id === foregroundedTaskId));
+function getSelectableBackgroundTasks(
+ tasks: Record | undefined,
+ foregroundedTaskId: string | undefined,
+): TaskState[] {
+ const backgroundTasks = Object.values(tasks ?? {}).filter(isBackgroundTask)
+ return backgroundTasks.filter(
+ task => !(task.type === 'local_agent' && task.id === foregroundedTaskId),
+ )
}
+
export function BackgroundTasksDialog({
onDone,
toolUseContext,
- initialDetailTaskId
+ initialDetailTaskId,
}: Props): React.ReactNode {
- const tasks = useAppState(s => s.tasks);
- const foregroundedTaskId = useAppState(s_0 => s_0.foregroundedTaskId);
- const showSpinnerTree = useAppState(s_1 => s_1.expandedView) === 'teammates';
- const setAppState = useSetAppState();
- const killAgentsShortcut = useShortcutDisplay('chat:killAgents', 'Chat', 'ctrl+x ctrl+k');
- const typedTasks = tasks as Record | undefined;
+ const tasks = useAppState(s => s.tasks)
+ const foregroundedTaskId = useAppState(s => s.foregroundedTaskId)
+ const showSpinnerTree = useAppState(s => s.expandedView) === 'teammates'
+ const setAppState = useSetAppState()
+ const killAgentsShortcut = useShortcutDisplay(
+ 'chat:killAgents',
+ 'Chat',
+ 'ctrl+x ctrl+k',
+ )
+ const typedTasks = tasks as Record | undefined
// Track if we skipped list view on mount (for back button behavior)
- const skippedListOnMount = useRef(false);
+ const skippedListOnMount = useRef(false)
// Compute initial view state - skip list if caller provided a specific task,
// or if there's exactly one task
const [viewState, setViewState] = useState(() => {
if (initialDetailTaskId) {
- skippedListOnMount.current = true;
- return {
- mode: 'detail',
- itemId: initialDetailTaskId
- };
+ skippedListOnMount.current = true
+ return { mode: 'detail', itemId: initialDetailTaskId }
}
- const allItems = getSelectableBackgroundTasks(typedTasks, foregroundedTaskId);
+ const allItems = getSelectableBackgroundTasks(
+ typedTasks,
+ foregroundedTaskId,
+ )
if (allItems.length === 1) {
- skippedListOnMount.current = true;
- return {
- mode: 'detail',
- itemId: allItems[0]!.id
- };
+ skippedListOnMount.current = true
+ return { mode: 'detail', itemId: allItems[0]!.id }
}
- return {
- mode: 'list'
- };
- });
- const [selectedIndex, setSelectedIndex] = useState(0);
+ return { mode: 'list' }
+ })
+ const [selectedIndex, setSelectedIndex] = useState(0)
// Register as modal overlay so parent Chat keybindings (up/down for history)
// are deactivated while this dialog is open
- useRegisterOverlay('background-tasks-dialog', undefined);
+ useRegisterOverlay('background-tasks-dialog')
// Memoize the sorted and categorized items together to ensure stable references
const {
@@ -175,37 +218,48 @@ export function BackgroundTasksDialog({
teammateTasks,
workflowTasks,
mcpMonitors,
- dreamTasks: dreamTasks_0,
- allSelectableItems
+ dreamTasks,
+ allSelectableItems,
} = useMemo(() => {
// Filter to only show running/pending background tasks, matching the status bar count
- const backgroundTasks = Object.values(typedTasks ?? {}).filter(isBackgroundTask);
- const allItems_0 = backgroundTasks.map(toListItem);
- const sorted = allItems_0.sort((a, b) => {
- const aStatus = a.status;
- const bStatus = b.status;
- if (aStatus === 'running' && bStatus !== 'running') return -1;
- if (aStatus !== 'running' && bStatus === 'running') return 1;
- const aTime = 'task' in a ? a.task.startTime : 0;
- const bTime = 'task' in b ? b.task.startTime : 0;
- return bTime - aTime;
- });
- const bash = sorted.filter(item => item.type === 'local_bash');
- const remote = sorted.filter(item_0 => item_0.type === 'remote_agent');
+ const backgroundTasks = Object.values(typedTasks ?? {}).filter(
+ isBackgroundTask,
+ )
+ const allItems = backgroundTasks.map(toListItem)
+ const sorted = allItems.sort((a, b) => {
+ const aStatus = a.status
+ const bStatus = b.status
+ if (aStatus === 'running' && bStatus !== 'running') return -1
+ if (aStatus !== 'running' && bStatus === 'running') return 1
+ const aTime = 'task' in a ? a.task.startTime : 0
+ const bTime = 'task' in b ? b.task.startTime : 0
+ return bTime - aTime
+ })
+ const bash = sorted.filter(item => item.type === 'local_bash')
+ const remote = sorted.filter(item => item.type === 'remote_agent')
// Exclude foregrounded task - it's being viewed in the main UI, not a background task
- const agent = sorted.filter(item_1 => item_1.type === 'local_agent' && item_1.id !== foregroundedTaskId);
- const workflows = sorted.filter(item_2 => item_2.type === 'local_workflow');
- const monitorMcp = sorted.filter(item_3 => item_3.type === 'monitor_mcp');
- const dreamTasks = sorted.filter(item_4 => item_4.type === 'dream');
+ const agent = sorted.filter(
+ item => item.type === 'local_agent' && item.id !== foregroundedTaskId,
+ )
+ const workflows = sorted.filter(item => item.type === 'local_workflow')
+ const monitorMcp = sorted.filter(item => item.type === 'monitor_mcp')
+ const dreamTasks = sorted.filter(item => item.type === 'dream')
// In spinner-tree mode, exclude teammates from the dialog (they appear in the tree)
- const teammates = showSpinnerTree ? [] : sorted.filter(item_5 => item_5.type === 'in_process_teammate');
+ const teammates = showSpinnerTree
+ ? []
+ : sorted.filter(item => item.type === 'in_process_teammate')
// Add leader entry when there are teammates, so users can foreground back to leader
- const leaderItem: ListItem[] = teammates.length > 0 ? [{
- id: '__leader__',
- type: 'leader',
- label: `@${TEAM_LEAD_NAME}`,
- status: 'running'
- }] : [];
+ const leaderItem: ListItem[] =
+ teammates.length > 0
+ ? [
+ {
+ id: '__leader__',
+ type: 'leader',
+ label: `@${TEAM_LEAD_NAME}`,
+ status: 'running',
+ },
+ ]
+ : []
return {
bashTasks: bash,
remoteSessions: remote,
@@ -217,135 +271,177 @@ export function BackgroundTasksDialog({
// Order MUST match JSX render order (teammates \u2192 bash \u2192 monitorMcp \u2192
// remote \u2192 agent \u2192 workflows \u2192 dream) so \u2193/\u2191 navigation moves the cursor
// visually downward.
- allSelectableItems: [...leaderItem, ...teammates, ...bash, ...monitorMcp, ...remote, ...agent, ...workflows, ...dreamTasks]
- };
- }, [typedTasks, foregroundedTaskId, showSpinnerTree]);
- const currentSelection = allSelectableItems[selectedIndex] ?? null;
+ allSelectableItems: [
+ ...leaderItem,
+ ...teammates,
+ ...bash,
+ ...monitorMcp,
+ ...remote,
+ ...agent,
+ ...workflows,
+ ...dreamTasks,
+ ],
+ }
+ }, [typedTasks, foregroundedTaskId, showSpinnerTree])
+
+ const currentSelection = allSelectableItems[selectedIndex] ?? null
// Use configurable keybindings for standard navigation and confirm/cancel.
// confirm:no is handled by Dialog's onCancel prop.
- useKeybindings({
- 'confirm:previous': () => setSelectedIndex(prev => Math.max(0, prev - 1)),
- 'confirm:next': () => setSelectedIndex(prev_0 => Math.min(allSelectableItems.length - 1, prev_0 + 1)),
- 'confirm:yes': () => {
- const current = allSelectableItems[selectedIndex];
- if (current) {
- if (current.type === 'leader') {
- exitTeammateView(setAppState);
- onDone('Viewing leader', {
- display: 'system'
- });
- } else {
- setViewState({
- mode: 'detail',
- itemId: current.id
- });
+ useKeybindings(
+ {
+ 'confirm:previous': () => setSelectedIndex(prev => Math.max(0, prev - 1)),
+ 'confirm:next': () =>
+ setSelectedIndex(prev =>
+ Math.min(allSelectableItems.length - 1, prev + 1),
+ ),
+ 'confirm:yes': () => {
+ const current = allSelectableItems[selectedIndex]
+ if (current) {
+ if (current.type === 'leader') {
+ exitTeammateView(setAppState)
+ onDone('Viewing leader', { display: 'system' })
+ } else {
+ setViewState({ mode: 'detail', itemId: current.id })
+ }
}
- }
- }
- }, {
- context: 'Confirmation',
- isActive: viewState.mode === 'list'
- });
+ },
+ },
+ { context: 'Confirmation', isActive: viewState.mode === 'list' },
+ )
// Component-specific shortcuts (x=stop, f=foreground, right=zoom) shown in UI.
// These are task-type and status dependent, not standard dialog keybindings.
const handleKeyDown = (e: KeyboardEvent) => {
// Only handle input when in list mode
- if (viewState.mode !== 'list') return;
+ if (viewState.mode !== 'list') return
+
if (e.key === 'left') {
- e.preventDefault();
- onDone('Background tasks dialog dismissed', {
- display: 'system'
- });
- return;
+ e.preventDefault()
+ onDone('Background tasks dialog dismissed', { display: 'system' })
+ return
}
// Compute current selection at the time of the key press
- const currentSelection_0 = allSelectableItems[selectedIndex];
- if (!currentSelection_0) return; // everything below requires a selection
+ const currentSelection = allSelectableItems[selectedIndex]
+ if (!currentSelection) return // everything below requires a selection
if (e.key === 'x') {
- e.preventDefault();
- if (currentSelection_0.type === 'local_bash' && currentSelection_0.status === 'running') {
- void killShellTask(currentSelection_0.id);
- } else if (currentSelection_0.type === 'local_agent' && currentSelection_0.status === 'running') {
- void killAgentTask(currentSelection_0.id);
- } else if (currentSelection_0.type === 'in_process_teammate' && currentSelection_0.status === 'running') {
- void killTeammateTask(currentSelection_0.id);
- } else if (currentSelection_0.type === 'local_workflow' && currentSelection_0.status === 'running' && killWorkflowTask) {
- killWorkflowTask(currentSelection_0.id, setAppState);
- } else if (currentSelection_0.type === 'monitor_mcp' && currentSelection_0.status === 'running' && killMonitorMcp) {
- killMonitorMcp(currentSelection_0.id, setAppState);
- } else if (currentSelection_0.type === 'dream' && currentSelection_0.status === 'running') {
- void killDreamTask(currentSelection_0.id);
- } else if (currentSelection_0.type === 'remote_agent' && currentSelection_0.status === 'running') {
- if (currentSelection_0.task.isUltraplan) {
- void stopUltraplan(currentSelection_0.id, currentSelection_0.task.sessionId, setAppState);
+ e.preventDefault()
+ if (
+ currentSelection.type === 'local_bash' &&
+ currentSelection.status === 'running'
+ ) {
+ void killShellTask(currentSelection.id)
+ } else if (
+ currentSelection.type === 'local_agent' &&
+ currentSelection.status === 'running'
+ ) {
+ void killAgentTask(currentSelection.id)
+ } else if (
+ currentSelection.type === 'in_process_teammate' &&
+ currentSelection.status === 'running'
+ ) {
+ void killTeammateTask(currentSelection.id)
+ } else if (
+ currentSelection.type === 'local_workflow' &&
+ currentSelection.status === 'running' &&
+ killWorkflowTask
+ ) {
+ killWorkflowTask(currentSelection.id, setAppState)
+ } else if (
+ currentSelection.type === 'monitor_mcp' &&
+ currentSelection.status === 'running' &&
+ killMonitorMcp
+ ) {
+ killMonitorMcp(currentSelection.id, setAppState)
+ } else if (
+ currentSelection.type === 'dream' &&
+ currentSelection.status === 'running'
+ ) {
+ void killDreamTask(currentSelection.id)
+ } else if (
+ currentSelection.type === 'remote_agent' &&
+ currentSelection.status === 'running'
+ ) {
+ if (currentSelection.task.isUltraplan) {
+ void stopUltraplan(
+ currentSelection.id,
+ currentSelection.task.sessionId,
+ setAppState,
+ )
} else {
- void killRemoteAgentTask(currentSelection_0.id);
+ void killRemoteAgentTask(currentSelection.id)
}
}
}
+
if (e.key === 'f') {
- if (currentSelection_0.type === 'in_process_teammate' && currentSelection_0.status === 'running') {
- e.preventDefault();
- enterTeammateView(currentSelection_0.id, setAppState);
- onDone('Viewing teammate', {
- display: 'system'
- });
- } else if (currentSelection_0.type === 'leader') {
- e.preventDefault();
- exitTeammateView(setAppState);
- onDone('Viewing leader', {
- display: 'system'
- });
+ if (
+ currentSelection.type === 'in_process_teammate' &&
+ currentSelection.status === 'running'
+ ) {
+ e.preventDefault()
+ enterTeammateView(currentSelection.id, setAppState)
+ onDone('Viewing teammate', { display: 'system' })
+ } else if (currentSelection.type === 'leader') {
+ e.preventDefault()
+ exitTeammateView(setAppState)
+ onDone('Viewing leader', { display: 'system' })
}
}
- };
+ }
+
async function killShellTask(taskId: string): Promise {
- await LocalShellTask.kill(taskId, setAppState);
+ await LocalShellTask.kill(taskId, setAppState)
}
- async function killAgentTask(taskId_0: string): Promise {
- await LocalAgentTask.kill(taskId_0, setAppState);
+
+ async function killAgentTask(taskId: string): Promise {
+ await LocalAgentTask.kill(taskId, setAppState)
}
- async function killTeammateTask(taskId_1: string): Promise {
- await InProcessTeammateTask.kill(taskId_1, setAppState);
+
+ async function killTeammateTask(taskId: string): Promise {
+ await InProcessTeammateTask.kill(taskId, setAppState)
}
- async function killDreamTask(taskId_2: string): Promise {
- await DreamTask.kill(taskId_2, setAppState);
+
+ async function killDreamTask(taskId: string): Promise {
+ await DreamTask.kill(taskId, setAppState)
}
- async function killRemoteAgentTask(taskId_3: string): Promise {
- await RemoteAgentTask.kill(taskId_3, setAppState);
+
+ async function killRemoteAgentTask(taskId: string): Promise {
+ await RemoteAgentTask.kill(taskId, setAppState)
}
// Wrap onDone in useEffectEvent to get a stable reference that always calls
// the current onDone callback without causing the effect to re-fire.
- const onDoneEvent = useEffectEvent(onDone);
+ const onDoneEvent = useEffectEvent(onDone)
+
useEffect(() => {
if (viewState.mode !== 'list') {
- const task = (typedTasks ?? {})[viewState.itemId];
+ const task = (typedTasks ?? {})[viewState.itemId]
// Workflow tasks get a grace: their detail view stays open through
// completion so the user sees the final state before eviction.
- if (!task || task.type !== 'local_workflow' && !isBackgroundTask(task)) {
+ if (
+ !task ||
+ (task.type !== 'local_workflow' && !isBackgroundTask(task))
+ ) {
// Task was removed or is no longer a background task (e.g. killed).
// If we skipped the list on mount, close the dialog entirely.
if (skippedListOnMount.current) {
onDoneEvent('Background tasks dialog dismissed', {
- display: 'system'
- });
+ display: 'system',
+ })
} else {
- setViewState({
- mode: 'list'
- });
+ setViewState({ mode: 'list' })
}
}
}
- const totalItems = allSelectableItems.length;
+
+ const totalItems = allSelectableItems.length
if (selectedIndex >= totalItems && totalItems > 0) {
- setSelectedIndex(totalItems - 1);
+ setSelectedIndex(totalItems - 1)
}
- }, [viewState, typedTasks, selectedIndex, allSelectableItems, onDoneEvent]);
+ }, [viewState, typedTasks, selectedIndex, allSelectableItems, onDoneEvent])
// Helper to go back to list view (or close dialog if we skipped list on
// mount AND there's still only ≤1 item). Checking current count prevents
@@ -353,142 +449,421 @@ export function BackgroundTasksDialog({
// then a second task started, 'back' should show the list — not close.
const goBackToList = () => {
if (skippedListOnMount.current && allSelectableItems.length <= 1) {
- onDone('Background tasks dialog dismissed', {
- display: 'system'
- });
+ onDone('Background tasks dialog dismissed', { display: 'system' })
} else {
- skippedListOnMount.current = false;
- setViewState({
- mode: 'list'
- });
+ skippedListOnMount.current = false
+ setViewState({ mode: 'list' })
}
- };
+ }
// If an item is selected, show the appropriate view
if (viewState.mode !== 'list' && typedTasks) {
- const task_0 = typedTasks[viewState.itemId];
- if (!task_0) {
- return null;
+ const task = typedTasks[viewState.itemId]
+ if (!task) {
+ return null
}
// Detail mode - show appropriate detail dialog
- switch (task_0.type) {
+ switch (task.type) {
case 'local_bash':
- return void killShellTask(task_0.id)} onBack={goBackToList} key={`shell-${task_0.id}`} />;
+ return (
+ void killShellTask(task.id)}
+ onBack={goBackToList}
+ key={`shell-${task.id}`}
+ />
+ )
case 'local_agent':
- return void killAgentTask(task_0.id)} onBack={goBackToList} key={`agent-${task_0.id}`} />;
+ return (
+ void killAgentTask(task.id)}
+ onBack={goBackToList}
+ key={`agent-${task.id}`}
+ />
+ )
case 'remote_agent':
- return void stopUltraplan(task_0.id, task_0.sessionId, setAppState) : () => void killRemoteAgentTask(task_0.id)} key={`session-${task_0.id}`} />;
+ return (
+
+ void stopUltraplan(task.id, task.sessionId, setAppState)
+ : () => void killRemoteAgentTask(task.id)
+ }
+ key={`session-${task.id}`}
+ />
+ )
case 'in_process_teammate':
- return void killTeammateTask(task_0.id) : undefined} onBack={goBackToList} onForeground={task_0.status === 'running' ? () => {
- enterTeammateView(task_0.id, setAppState);
- onDone('Viewing teammate', {
- display: 'system'
- });
- } : undefined} key={`teammate-${task_0.id}`} />;
+ return (
+ void killTeammateTask(task.id)
+ : undefined
+ }
+ onBack={goBackToList}
+ onForeground={
+ task.status === 'running'
+ ? () => {
+ enterTeammateView(task.id, setAppState)
+ onDone('Viewing teammate', { display: 'system' })
+ }
+ : undefined
+ }
+ key={`teammate-${task.id}`}
+ />
+ )
case 'local_workflow':
- if (!WorkflowDetailDialog) return null;
- return killWorkflowTask(task_0.id, setAppState) : undefined} onSkipAgent={task_0.status === 'running' && skipWorkflowAgent ? agentId => skipWorkflowAgent(task_0.id, agentId, setAppState) : undefined} onRetryAgent={task_0.status === 'running' && retryWorkflowAgent ? agentId_0 => retryWorkflowAgent(task_0.id, agentId_0, setAppState) : undefined} onBack={goBackToList} key={`workflow-${task_0.id}`} />;
+ if (!WorkflowDetailDialog) return null
+ return (
+ killWorkflowTask(task.id, setAppState)
+ : undefined
+ }
+ onSkipAgent={
+ task.status === 'running' && skipWorkflowAgent
+ ? agentId => skipWorkflowAgent(task.id, agentId, setAppState)
+ : undefined
+ }
+ onRetryAgent={
+ task.status === 'running' && retryWorkflowAgent
+ ? agentId => retryWorkflowAgent(task.id, agentId, setAppState)
+ : undefined
+ }
+ onBack={goBackToList}
+ key={`workflow-${task.id}`}
+ />
+ )
case 'monitor_mcp':
- if (!MonitorMcpDetailDialog) return null;
- return killMonitorMcp(task_0.id, setAppState) : undefined} onBack={goBackToList} key={`monitor-mcp-${task_0.id}`} />;
+ if (!MonitorMcpDetailDialog) return null
+ return (
+ killMonitorMcp(task.id, setAppState)
+ : undefined
+ }
+ onBack={goBackToList}
+ key={`monitor-mcp-${task.id}`}
+ />
+ )
case 'dream':
- return onDone('Background tasks dialog dismissed', {
- display: 'system'
- })} onBack={goBackToList} onKill={task_0.status === 'running' ? () => void killDreamTask(task_0.id) : undefined} key={`dream-${task_0.id}`} />;
+ return (
+
+ onDone('Background tasks dialog dismissed', {
+ display: 'system',
+ })
+ }
+ onBack={goBackToList}
+ onKill={
+ task.status === 'running'
+ ? () => void killDreamTask(task.id)
+ : undefined
+ }
+ key={`dream-${task.id}`}
+ />
+ )
}
}
- const runningBashCount = count(bashTasks, _ => _.status === 'running');
- const runningAgentCount = count(remoteSessions, __0 => __0.status === 'running' || __0.status === 'pending') + count(agentTasks, __1 => __1.status === 'running');
- const runningTeammateCount = count(teammateTasks, __2 => __2.status === 'running');
- const subtitle = intersperse([...(runningTeammateCount > 0 ? [
+
+ const runningBashCount = count(bashTasks, _ => _.status === 'running')
+ const runningAgentCount =
+ count(
+ remoteSessions,
+ _ => _.status === 'running' || _.status === 'pending',
+ ) + count(agentTasks, _ => _.status === 'running')
+ const runningTeammateCount = count(teammateTasks, _ => _.status === 'running')
+ const subtitle = intersperse(
+ [
+ ...(runningTeammateCount > 0
+ ? [
+
{runningTeammateCount}{' '}
{runningTeammateCount !== 1 ? 'agents' : 'agent'}
- ] : []), ...(runningBashCount > 0 ? [
+ ,
+ ]
+ : []),
+ ...(runningBashCount > 0
+ ? [
+
{runningBashCount}{' '}
{runningBashCount !== 1 ? 'active shells' : 'active shell'}
- ] : []), ...(runningAgentCount > 0 ? [
+ ,
+ ]
+ : []),
+ ...(runningAgentCount > 0
+ ? [
+
{runningAgentCount}{' '}
{runningAgentCount !== 1 ? 'active agents' : 'active agent'}
- ] : [])], index => · );
- const actions = [, , ...(currentSelection?.type === 'in_process_teammate' && currentSelection.status === 'running' ? [] : []), ...((currentSelection?.type === 'local_bash' || currentSelection?.type === 'local_agent' || currentSelection?.type === 'in_process_teammate' || currentSelection?.type === 'local_workflow' || currentSelection?.type === 'monitor_mcp' || currentSelection?.type === 'dream' || currentSelection?.type === 'remote_agent') && currentSelection.status === 'running' ? [] : []), ...(agentTasks.some(t => t.status === 'running') ? [] : []), ];
- const handleCancel = () => onDone('Background tasks dialog dismissed', {
- display: 'system'
- });
+ ,
+ ]
+ : []),
+ ],
+ index => · ,
+ )
+
+ const actions = [
+ ,
+ ,
+ ...(currentSelection?.type === 'in_process_teammate' &&
+ currentSelection.status === 'running'
+ ? [
+ ,
+ ]
+ : []),
+ ...((currentSelection?.type === 'local_bash' ||
+ currentSelection?.type === 'local_agent' ||
+ currentSelection?.type === 'in_process_teammate' ||
+ currentSelection?.type === 'local_workflow' ||
+ currentSelection?.type === 'monitor_mcp' ||
+ currentSelection?.type === 'dream' ||
+ currentSelection?.type === 'remote_agent') &&
+ currentSelection.status === 'running'
+ ? []
+ : []),
+ ...(agentTasks.some(t => t.status === 'running')
+ ? [
+ ,
+ ]
+ : []),
+ ,
+ ]
+
+ const handleCancel = () =>
+ onDone('Background tasks dialog dismissed', { display: 'system' })
+
function renderInputGuide(exitState: ExitState): React.ReactNode {
if (exitState.pending) {
- return Press {exitState.keyName} again to exit;
+ return Press {exitState.keyName} again to exit
}
- return {actions};
+ return {actions}
}
- return
- {subtitle}>} onCancel={handleCancel} color="background" inputGuide={renderInputGuide}>
- {allSelectableItems.length === 0 ? No tasks currently running :
- {teammateTasks.length > 0 &&
- {(bashTasks.length > 0 || remoteSessions.length > 0 || agentTasks.length > 0) &&
+
+ return (
+
+ {subtitle}>}
+ onCancel={handleCancel}
+ color="background"
+ inputGuide={renderInputGuide}
+ >
+ {allSelectableItems.length === 0 ? (
+ No tasks currently running
+ ) : (
+
+ {teammateTasks.length > 0 && (
+
+ {(bashTasks.length > 0 ||
+ remoteSessions.length > 0 ||
+ agentTasks.length > 0) && (
+
{' '}Agents (
{count(teammateTasks, i => i.type !== 'leader')})
- }
+
+ )}
-
+
- }
+
+ )}
- {bashTasks.length > 0 && 0 ? 1 : 0}>
- {(teammateTasks.length > 0 || remoteSessions.length > 0 || agentTasks.length > 0) &&
+ {bashTasks.length > 0 && (
+ 0 ? 1 : 0}
+ >
+ {(teammateTasks.length > 0 ||
+ remoteSessions.length > 0 ||
+ agentTasks.length > 0) && (
+
{' '}Shells ({bashTasks.length})
- }
+
+ )}
- {bashTasks.map(item_6 => )}
+ {bashTasks.map(item => (
+
+ ))}
- }
+
+ )}
- {mcpMonitors.length > 0 && 0 || bashTasks.length > 0 ? 1 : 0}>
+ {mcpMonitors.length > 0 && (
+ 0 || bashTasks.length > 0 ? 1 : 0
+ }
+ >
{' '}Monitors ({mcpMonitors.length})
- {mcpMonitors.map(item_7 => )}
+ {mcpMonitors.map(item => (
+
+ ))}
- }
+
+ )}
- {remoteSessions.length > 0 && 0 || bashTasks.length > 0 || mcpMonitors.length > 0 ? 1 : 0}>
+ {remoteSessions.length > 0 && (
+ 0 ||
+ bashTasks.length > 0 ||
+ mcpMonitors.length > 0
+ ? 1
+ : 0
+ }
+ >
{' '}Remote agents ({remoteSessions.length}
)
- {remoteSessions.map(item_8 => )}
+ {remoteSessions.map(item => (
+
+ ))}
- }
+
+ )}
- {agentTasks.length > 0 && 0 || bashTasks.length > 0 || mcpMonitors.length > 0 || remoteSessions.length > 0 ? 1 : 0}>
+ {agentTasks.length > 0 && (
+ 0 ||
+ bashTasks.length > 0 ||
+ mcpMonitors.length > 0 ||
+ remoteSessions.length > 0
+ ? 1
+ : 0
+ }
+ >
{' '}Local agents ({agentTasks.length})
- {agentTasks.map(item_9 => )}
+ {agentTasks.map(item => (
+
+ ))}
- }
+
+ )}
- {workflowTasks.length > 0 && 0 || bashTasks.length > 0 || mcpMonitors.length > 0 || remoteSessions.length > 0 || agentTasks.length > 0 ? 1 : 0}>
+ {workflowTasks.length > 0 && (
+ 0 ||
+ bashTasks.length > 0 ||
+ mcpMonitors.length > 0 ||
+ remoteSessions.length > 0 ||
+ agentTasks.length > 0
+ ? 1
+ : 0
+ }
+ >
{' '}Workflows ({workflowTasks.length})
- {workflowTasks.map(item_10 => )}
+ {workflowTasks.map(item => (
+
+ ))}
- }
+
+ )}
- {dreamTasks_0.length > 0 && 0 || bashTasks.length > 0 || mcpMonitors.length > 0 || remoteSessions.length > 0 || agentTasks.length > 0 || workflowTasks.length > 0 ? 1 : 0}>
+ {dreamTasks.length > 0 && (
+ 0 ||
+ bashTasks.length > 0 ||
+ mcpMonitors.length > 0 ||
+ remoteSessions.length > 0 ||
+ agentTasks.length > 0 ||
+ workflowTasks.length > 0
+ ? 1
+ : 0
+ }
+ >
- {dreamTasks_0.map(item_11 => )}
+ {dreamTasks.map(item => (
+
+ ))}
- }
- }
+
+ )}
+
+ )}
- ;
+
+ )
}
+
function toListItem(task: BackgroundTaskState): ListItem {
switch (task.type) {
case 'local_bash':
@@ -497,155 +872,141 @@ function toListItem(task: BackgroundTaskState): ListItem {
type: 'local_bash',
label: task.kind === 'monitor' ? task.description : task.command,
status: task.status,
- task
- };
+ task,
+ }
case 'remote_agent':
return {
id: task.id,
type: 'remote_agent',
label: task.title,
status: task.status,
- task
- };
+ task,
+ }
case 'local_agent':
return {
id: task.id,
type: 'local_agent',
label: task.description,
status: task.status,
- task
- };
+ task,
+ }
case 'in_process_teammate':
return {
id: task.id,
type: 'in_process_teammate',
label: `@${task.identity.agentName}`,
status: task.status,
- task
- };
+ task,
+ }
case 'local_workflow':
return {
id: task.id,
type: 'local_workflow',
label: task.summary ?? task.description,
status: task.status,
- task
- };
+ task,
+ }
case 'monitor_mcp':
return {
id: task.id,
type: 'monitor_mcp',
label: task.description,
status: task.status,
- task
- };
+ task,
+ }
case 'dream':
return {
id: task.id,
type: 'dream',
label: task.description,
status: task.status,
- task
- };
- }
-}
-function Item(t0) {
- const $ = _c(14);
- const {
- item,
- isSelected
- } = t0;
- const {
- columns
- } = useTerminalSize();
- const maxActivityWidth = Math.max(30, columns - 26);
- let t1;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = isCoordinatorMode();
- $[0] = t1;
- } else {
- t1 = $[0];
- }
- const useGreyPointer = t1;
- const t2 = useGreyPointer && isSelected;
- const t3 = isSelected ? figures.pointer + " " : " ";
- let t4;
- if ($[1] !== t2 || $[2] !== t3) {
- t4 = {t3};
- $[1] = t2;
- $[2] = t3;
- $[3] = t4;
- } else {
- t4 = $[3];
- }
- const t5 = isSelected && !useGreyPointer ? "suggestion" : undefined;
- let t6;
- if ($[4] !== item.task || $[5] !== item.type || $[6] !== maxActivityWidth) {
- t6 = item.type === "leader" ? @{TEAM_LEAD_NAME} : ;
- $[4] = item.task;
- $[5] = item.type;
- $[6] = maxActivityWidth;
- $[7] = t6;
- } else {
- t6 = $[7];
- }
- let t7;
- if ($[8] !== t5 || $[9] !== t6) {
- t7 = {t6};
- $[8] = t5;
- $[9] = t6;
- $[10] = t7;
- } else {
- t7 = $[10];
- }
- let t8;
- if ($[11] !== t4 || $[12] !== t7) {
- t8 = {t4}{t7};
- $[11] = t4;
- $[12] = t7;
- $[13] = t8;
- } else {
- t8 = $[13];
- }
- return t8;
-}
-function TeammateTaskGroups(t0) {
- const $ = _c(3);
- const {
- teammateTasks,
- currentSelectionId
- } = t0;
- let t1;
- if ($[0] !== currentSelectionId || $[1] !== teammateTasks) {
- const leaderItems = teammateTasks.filter(_temp);
- const teammateItems = teammateTasks.filter(_temp2);
- const teams = new Map();
- for (const item of teammateItems) {
- const teamName = item.task.identity.teamName;
- const group = teams.get(teamName);
- if (group) {
- group.push(item);
- } else {
- teams.set(teamName, [item]);
+ task,
}
- }
- const teamEntries = [...teams.entries()];
- t1 = <>{teamEntries.map(t2 => {
- const [teamName_0, items] = t2;
- const memberCount = items.length + leaderItems.length;
- return {" "}Team: {teamName_0} ({memberCount}){leaderItems.map(item_0 => )}{items.map(item_1 => )};
- })}>;
- $[0] = currentSelectionId;
- $[1] = teammateTasks;
- $[2] = t1;
- } else {
- t1 = $[2];
}
- return t1;
}
-function _temp2(i_0) {
- return i_0.type === "in_process_teammate";
+
+function Item({
+ item,
+ isSelected,
+}: {
+ item: ListItem
+ isSelected: boolean
+}): ReactNode {
+ const { columns } = useTerminalSize()
+ // Dialog border (2) + padding (2) + pointer prefix (2) + name/status overhead (~20)
+ const maxActivityWidth = Math.max(30, columns - 26)
+ // In coordinator mode, use grey pointer instead of blue
+ const useGreyPointer = isCoordinatorMode()
+
+ return (
+
+
+ {isSelected ? figures.pointer + ' ' : ' '}
+
+
+ {item.type === 'leader' ? (
+ @{TEAM_LEAD_NAME}
+ ) : (
+
+ )}
+
+
+ )
}
-function _temp(i) {
- return i.type === "leader";
+
+function TeammateTaskGroups({
+ teammateTasks,
+ currentSelectionId,
+}: {
+ teammateTasks: ListItem[]
+ currentSelectionId: string | undefined
+}): ReactNode {
+ // Separate leader from teammates, group teammates by team
+ const leaderItems = teammateTasks.filter(i => i.type === 'leader')
+ const teammateItems = teammateTasks.filter(
+ i => i.type === 'in_process_teammate',
+ )
+ const teams = new Map()
+ for (const item of teammateItems) {
+ const teamName = item.task.identity.teamName
+ const group = teams.get(teamName)
+ if (group) {
+ group.push(item)
+ } else {
+ teams.set(teamName, [item])
+ }
+ }
+ const teamEntries = [...teams.entries()]
+ return (
+ <>
+ {teamEntries.map(([teamName, items]) => {
+ const memberCount = items.length + leaderItems.length
+ return (
+
+
+ {' '}Team: {teamName} ({memberCount})
+
+ {/* Render leader first within each team */}
+ {leaderItems.map(item => (
+
+ ))}
+ {items.map(item => (
+
+ ))}
+
+ )
+ })}
+ >
+ )
}
diff --git a/src/components/tasks/DreamDetailDialog.tsx b/src/components/tasks/DreamDetailDialog.tsx
index 46470c000..bea310946 100644
--- a/src/components/tasks/DreamDetailDialog.tsx
+++ b/src/components/tasks/DreamDetailDialog.tsx
@@ -1,250 +1,136 @@
-import { c as _c } from "react/compiler-runtime";
-import React from 'react';
-import type { DeepImmutable } from 'src/types/utils.js';
-import { useElapsedTime } from '../../hooks/useElapsedTime.js';
-import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
-import { Box, Text } from '../../ink.js';
-import { useKeybindings } from '../../keybindings/useKeybinding.js';
-import type { DreamTaskState } from '../../tasks/DreamTask/DreamTask.js';
-import { plural } from '../../utils/stringUtils.js';
-import { Byline } from '../design-system/Byline.js';
-import { Dialog } from '../design-system/Dialog.js';
-import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
+import React from 'react'
+import type { DeepImmutable } from 'src/types/utils.js'
+import { useElapsedTime } from '../../hooks/useElapsedTime.js'
+import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
+import { Box, Text } from '../../ink.js'
+import { useKeybindings } from '../../keybindings/useKeybinding.js'
+import type { DreamTaskState } from '../../tasks/DreamTask/DreamTask.js'
+import { plural } from '../../utils/stringUtils.js'
+import { Byline } from '../design-system/Byline.js'
+import { Dialog } from '../design-system/Dialog.js'
+import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
+
type Props = {
- task: DeepImmutable;
- onDone: () => void;
- onBack?: () => void;
- onKill?: () => void;
-};
+ task: DeepImmutable
+ onDone: () => void
+ onBack?: () => void
+ onKill?: () => void
+}
// How many recent turns to render. Earlier turns collapse to a count.
-const VISIBLE_TURNS = 6;
-export function DreamDetailDialog(t0) {
- const $ = _c(70);
- const {
- task,
- onDone,
- onBack,
- onKill
- } = t0;
- const elapsedTime = useElapsedTime(task.startTime, task.status === "running", 1000, 0);
- let t1;
- if ($[0] !== onDone) {
- t1 = {
- "confirm:yes": onDone
- };
- $[0] = onDone;
- $[1] = t1;
- } else {
- t1 = $[1];
+const VISIBLE_TURNS = 6
+
+export function DreamDetailDialog({
+ task,
+ onDone,
+ onBack,
+ onKill,
+}: Props): React.ReactNode {
+ const elapsedTime = useElapsedTime(
+ task.startTime,
+ task.status === 'running',
+ 1000,
+ 0,
+ )
+
+ // Dialog handles confirm:no (Esc) → onCancel. Wire confirm:yes (Enter/y) too.
+ useKeybindings({ 'confirm:yes': onDone }, { context: 'Confirmation' })
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === ' ') {
+ e.preventDefault()
+ onDone()
+ } else if (e.key === 'left' && onBack) {
+ e.preventDefault()
+ onBack()
+ } else if (e.key === 'x' && task.status === 'running' && onKill) {
+ e.preventDefault()
+ onKill()
+ }
}
- let t2;
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
- t2 = {
- context: "Confirmation"
- };
- $[2] = t2;
- } else {
- t2 = $[2];
- }
- useKeybindings(t1, t2);
- let t3;
- if ($[3] !== onBack || $[4] !== onDone || $[5] !== onKill || $[6] !== task.status) {
- t3 = e => {
- if (e.key === " ") {
- e.preventDefault();
- onDone();
- } else {
- if (e.key === "left" && onBack) {
- e.preventDefault();
- onBack();
- } else {
- if (e.key === "x" && task.status === "running" && onKill) {
- e.preventDefault();
- onKill();
- }
+
+ // Turns with text to show. Tool-only turns (text='') are dropped entirely —
+ // the per-turn toolUseCount already captures that work.
+ const visibleTurns = task.turns.filter(t => t.text !== '')
+ const shown = visibleTurns.slice(-VISIBLE_TURNS)
+ const hidden = visibleTurns.length - shown.length
+
+ return (
+
+
+ {elapsedTime} · reviewing {task.sessionsReviewing}{' '}
+ {plural(task.sessionsReviewing, 'session')}
+ {task.filesTouched.length > 0 && (
+ <>
+ {' '}
+ · {task.filesTouched.length}{' '}
+ {plural(task.filesTouched.length, 'file')} touched
+ >
+ )}
+
}
- }
- };
- $[3] = onBack;
- $[4] = onDone;
- $[5] = onKill;
- $[6] = task.status;
- $[7] = t3;
- } else {
- t3 = $[7];
- }
- const handleKeyDown = t3;
- let T0;
- let T1;
- let T2;
- let t10;
- let t11;
- let t12;
- let t13;
- let t14;
- let t15;
- let t16;
- let t4;
- let t5;
- let t6;
- let t7;
- let t8;
- let t9;
- if ($[8] !== elapsedTime || $[9] !== handleKeyDown || $[10] !== onBack || $[11] !== onDone || $[12] !== onKill || $[13] !== task.filesTouched.length || $[14] !== task.sessionsReviewing || $[15] !== task.status || $[16] !== task.turns) {
- const visibleTurns = task.turns.filter(_temp);
- const shown = visibleTurns.slice(-VISIBLE_TURNS);
- const hidden = visibleTurns.length - shown.length;
- T2 = Box;
- t13 = "column";
- t14 = 0;
- t15 = true;
- t16 = handleKeyDown;
- T1 = Dialog;
- t8 = "Memory consolidation";
- const t17 = task.sessionsReviewing;
- let t18;
- if ($[33] !== task.sessionsReviewing) {
- t18 = plural(task.sessionsReviewing, "session");
- $[33] = task.sessionsReviewing;
- $[34] = t18;
- } else {
- t18 = $[34];
- }
- let t19;
- if ($[35] !== task.filesTouched.length) {
- t19 = task.filesTouched.length > 0 && <>{" "}· {task.filesTouched.length}{" "}{plural(task.filesTouched.length, "file")} touched>;
- $[35] = task.filesTouched.length;
- $[36] = t19;
- } else {
- t19 = $[36];
- }
- if ($[37] !== elapsedTime || $[38] !== t18 || $[39] !== t19 || $[40] !== task.sessionsReviewing) {
- t9 = {elapsedTime} · reviewing {t17}{" "}{t18}{t19};
- $[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 ? Press {exitState.keyName} again to exit : {onBack && }{task.status === "running" && onKill && };
- $[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 = Status:;
- $[46] = t20;
- } else {
- t20 = $[46];
- }
- if ($[47] !== task.status) {
- t6 = {t20}{" "}{task.status === "running" ? running : task.status === "completed" ? {task.status} : {task.status}};
- $[47] = task.status;
- $[48] = t6;
- } else {
- t6 = $[48];
- }
- t7 = shown.length === 0 ? {task.status === "running" ? "Starting\u2026" : "(no text output)"} : <>{hidden > 0 && ({hidden} earlier {plural(hidden, "turn")})}{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 = {t6}{t7};
- $[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 = {t17};
- $[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 = {t18};
- $[63] = T2;
- $[64] = t13;
- $[65] = t14;
- $[66] = t15;
- $[67] = t16;
- $[68] = t18;
- $[69] = t19;
- } else {
- t19 = $[69];
- }
- return t19;
-}
-function _temp2(turn, i) {
- return {turn.text}{turn.toolUseCount > 0 && {" "}({turn.toolUseCount}{" "}{plural(turn.toolUseCount, "tool")})};
-}
-function _temp(t) {
- return t.text !== "";
+ onCancel={onDone}
+ color="background"
+ inputGuide={exitState =>
+ exitState.pending ? (
+ Press {exitState.keyName} again to exit
+ ) : (
+
+ {onBack && }
+
+ {task.status === 'running' && onKill && (
+
+ )}
+
+ )
+ }
+ >
+
+
+ Status:{' '}
+ {task.status === 'running' ? (
+ running
+ ) : task.status === 'completed' ? (
+ {task.status}
+ ) : (
+ {task.status}
+ )}
+
+
+ {shown.length === 0 ? (
+
+ {task.status === 'running' ? 'Starting…' : '(no text output)'}
+
+ ) : (
+ <>
+ {hidden > 0 && (
+
+ ({hidden} earlier {plural(hidden, 'turn')})
+
+ )}
+ {shown.map((turn, i) => (
+
+ {turn.text}
+ {turn.toolUseCount > 0 && (
+
+ {' '}({turn.toolUseCount}{' '}
+ {plural(turn.toolUseCount, 'tool')})
+
+ )}
+
+ ))}
+ >
+ )}
+
+
+
+ )
}
diff --git a/src/components/tasks/InProcessTeammateDetailDialog.tsx b/src/components/tasks/InProcessTeammateDetailDialog.tsx
index c8b25f80f..b59bbbd5e 100644
--- a/src/components/tasks/InProcessTeammateDetailDialog.tsx
+++ b/src/components/tasks/InProcessTeammateDetailDialog.tsx
@@ -1,265 +1,193 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { useMemo } from 'react';
-import type { DeepImmutable } from 'src/types/utils.js';
-import { useElapsedTime } from '../../hooks/useElapsedTime.js';
-import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
-import { Box, Text, useTheme } from '../../ink.js';
-import { useKeybindings } from '../../keybindings/useKeybinding.js';
-import { getEmptyToolPermissionContext } from '../../Tool.js';
-import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js';
-import { getTools } from '../../tools.js';
-import { formatNumber, truncateToWidth } from '../../utils/format.js';
-import { toInkColor } from '../../utils/ink.js';
-import { Byline } from '../design-system/Byline.js';
-import { Dialog } from '../design-system/Dialog.js';
-import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
-import { renderToolActivity } from './renderToolActivity.js';
-import { describeTeammateActivity } from './taskStatusUtils.js';
+import React, { useMemo } from 'react'
+import type { DeepImmutable } from 'src/types/utils.js'
+import { useElapsedTime } from '../../hooks/useElapsedTime.js'
+import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
+import { Box, Text, useTheme } from '../../ink.js'
+import { useKeybindings } from '../../keybindings/useKeybinding.js'
+import { getEmptyToolPermissionContext } from '../../Tool.js'
+import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js'
+import { getTools } from '../../tools.js'
+import { formatNumber, truncateToWidth } from '../../utils/format.js'
+import { toInkColor } from '../../utils/ink.js'
+import { Byline } from '../design-system/Byline.js'
+import { Dialog } from '../design-system/Dialog.js'
+import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
+import { renderToolActivity } from './renderToolActivity.js'
+import { describeTeammateActivity } from './taskStatusUtils.js'
+
type Props = {
- teammate: DeepImmutable;
- onDone: () => void;
- onKill?: () => void;
- onBack?: () => void;
- onForeground?: () => void;
-};
-export function InProcessTeammateDetailDialog(t0) {
- const $ = _c(63);
- const {
- teammate,
- onDone,
- onKill,
- onBack,
- onForeground
- } = t0;
- const [theme] = useTheme();
- let t1;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = getTools(getEmptyToolPermissionContext());
- $[0] = t1;
- } else {
- t1 = $[0];
- }
- const tools = t1;
- const elapsedTime = useElapsedTime(teammate.startTime, teammate.status === "running", 1000, teammate.totalPausedMs ?? 0);
- let t2;
- if ($[1] !== onDone) {
- t2 = {
- "confirm:yes": onDone
- };
- $[1] = onDone;
- $[2] = t2;
- } else {
- t2 = $[2];
- }
- let t3;
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
- t3 = {
- context: "Confirmation"
- };
- $[3] = t3;
- } else {
- t3 = $[3];
- }
- useKeybindings(t2, t3);
- let t4;
- if ($[4] !== onBack || $[5] !== onDone || $[6] !== onForeground || $[7] !== onKill || $[8] !== teammate.status) {
- t4 = e => {
- if (e.key === " ") {
- e.preventDefault();
- onDone();
- } else {
- if (e.key === "left" && onBack) {
- e.preventDefault();
- onBack();
- } else {
- if (e.key === "x" && teammate.status === "running" && onKill) {
- e.preventDefault();
- onKill();
- } else {
- if (e.key === "f" && teammate.status === "running" && onForeground) {
- e.preventDefault();
- onForeground();
- }
- }
- }
- }
- };
- $[4] = onBack;
- $[5] = onDone;
- $[6] = onForeground;
- $[7] = onKill;
- $[8] = teammate.status;
- $[9] = t4;
- } else {
- t4 = $[9];
- }
- const handleKeyDown = t4;
- let t5;
- if ($[10] !== teammate) {
- t5 = describeTeammateActivity(teammate);
- $[10] = teammate;
- $[11] = t5;
- } else {
- t5 = $[11];
- }
- const activity = t5;
- const tokenCount = teammate.result?.totalTokens ?? teammate.progress?.tokenCount;
- const toolUseCount = teammate.result?.totalToolUseCount ?? teammate.progress?.toolUseCount;
- let t6;
- if ($[12] !== teammate.prompt) {
- t6 = truncateToWidth(teammate.prompt, 300);
- $[12] = teammate.prompt;
- $[13] = t6;
- } else {
- t6 = $[13];
- }
- const displayPrompt = t6;
- let t7;
- if ($[14] !== teammate.identity.color) {
- t7 = toInkColor(teammate.identity.color);
- $[14] = teammate.identity.color;
- $[15] = t7;
- } else {
- t7 = $[15];
- }
- let t8;
- if ($[16] !== t7 || $[17] !== teammate.identity.agentName) {
- t8 = @{teammate.identity.agentName};
- $[16] = t7;
- $[17] = teammate.identity.agentName;
- $[18] = t8;
- } else {
- t8 = $[18];
- }
- let t9;
- if ($[19] !== activity) {
- t9 = activity && ({activity});
- $[19] = activity;
- $[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 title = t10;
- let t11;
- if ($[24] !== teammate.status) {
- t11 = teammate.status !== "running" && {teammate.status === "completed" ? "Completed" : teammate.status === "failed" ? "Failed" : "Stopped"}{" \xB7 "};
- $[24] = teammate.status;
- $[25] = t11;
- } else {
- t11 = $[25];
- }
- let t12;
- if ($[26] !== tokenCount) {
- t12 = tokenCount !== undefined && tokenCount > 0 && <> · {formatNumber(tokenCount)} tokens>;
- $[26] = tokenCount;
- $[27] = t12;
- } else {
- t12 = $[27];
- }
- let t13;
- if ($[28] !== toolUseCount) {
- t13 = toolUseCount !== undefined && toolUseCount > 0 && <>{" "}· {toolUseCount} {toolUseCount === 1 ? "tool" : "tools"}>;
- $[28] = toolUseCount;
- $[29] = t13;
- } else {
- t13 = $[29];
- }
- let t14;
- if ($[30] !== elapsedTime || $[31] !== t12 || $[32] !== t13) {
- t14 = {elapsedTime}{t12}{t13};
- $[30] = elapsedTime;
- $[31] = t12;
- $[32] = t13;
- $[33] = t14;
- } else {
- t14 = $[33];
- }
- let t15;
- if ($[34] !== t11 || $[35] !== t14) {
- t15 = {t11}{t14};
- $[34] = t11;
- $[35] = t14;
- $[36] = t15;
- } else {
- t15 = $[36];
- }
- const subtitle = t15;
- let t16;
- if ($[37] !== onBack || $[38] !== onForeground || $[39] !== onKill || $[40] !== teammate.status) {
- t16 = exitState => exitState.pending ? Press {exitState.keyName} again to exit : {onBack && }{teammate.status === "running" && onKill && }{teammate.status === "running" && onForeground && };
- $[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 && Progress{teammate.progress.recentActivities.map((activity_0, i) => {i === teammate.progress.recentActivities.length - 1 ? "\u203A " : " "}{renderToolActivity(activity_0, tools, theme)})};
- $[42] = teammate.progress;
- $[43] = teammate.status;
- $[44] = theme;
- $[45] = t17;
- } else {
- t17 = $[45];
- }
- let t18;
- if ($[46] === Symbol.for("react.memo_cache_sentinel")) {
- t18 = Prompt;
- $[46] = t18;
- } else {
- t18 = $[46];
- }
- let t19;
- if ($[47] !== displayPrompt) {
- t19 = {t18}{displayPrompt};
- $[47] = displayPrompt;
- $[48] = t19;
- } else {
- t19 = $[48];
- }
- let t20;
- if ($[49] !== teammate.error || $[50] !== teammate.status) {
- t20 = teammate.status === "failed" && teammate.error && Error{teammate.error};
- $[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 = {t17}{t19}{t20};
- $[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 = {t21};
- $[60] = handleKeyDown;
- $[61] = t21;
- $[62] = t22;
- } else {
- t22 = $[62];
- }
- return t22;
+ teammate: DeepImmutable
+ onDone: () => void
+ onKill?: () => void
+ onBack?: () => void
+ onForeground?: () => void
+}
+export function InProcessTeammateDetailDialog({
+ teammate,
+ onDone,
+ onKill,
+ onBack,
+ onForeground,
+}: Props): React.ReactNode {
+ const [theme] = useTheme()
+ const tools = useMemo(() => getTools(getEmptyToolPermissionContext()), [])
+
+ const elapsedTime = useElapsedTime(
+ teammate.startTime,
+ teammate.status === 'running',
+ 1000,
+ teammate.totalPausedMs ?? 0,
+ )
+
+ // Restore confirm:yes (Enter/y) dismissal — Dialog handles confirm:no (Esc)
+ useKeybindings(
+ {
+ 'confirm:yes': onDone,
+ },
+ { context: 'Confirmation' },
+ )
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === ' ') {
+ e.preventDefault()
+ onDone()
+ } else if (e.key === 'left' && onBack) {
+ e.preventDefault()
+ onBack()
+ } else if (e.key === 'x' && teammate.status === 'running' && onKill) {
+ e.preventDefault()
+ onKill()
+ } else if (e.key === 'f' && teammate.status === 'running' && onForeground) {
+ e.preventDefault()
+ onForeground()
+ }
+ }
+
+ const activity = describeTeammateActivity(teammate)
+
+ const tokenCount =
+ teammate.result?.totalTokens ?? teammate.progress?.tokenCount
+ const toolUseCount =
+ teammate.result?.totalToolUseCount ?? teammate.progress?.toolUseCount
+
+ const displayPrompt = truncateToWidth(teammate.prompt, 300)
+
+ const title = (
+
+
+ @{teammate.identity.agentName}
+
+ {activity && ({activity})}
+
+ )
+
+ const subtitle = (
+
+ {teammate.status !== 'running' && (
+
+ {teammate.status === 'completed'
+ ? 'Completed'
+ : teammate.status === 'failed'
+ ? 'Failed'
+ : 'Stopped'}
+ {' · '}
+
+ )}
+
+ {elapsedTime}
+ {tokenCount !== undefined && tokenCount > 0 && (
+ <> · {formatNumber(tokenCount)} tokens>
+ )}
+ {toolUseCount !== undefined && toolUseCount > 0 && (
+ <>
+ {' '}
+ · {toolUseCount} {toolUseCount === 1 ? 'tool' : 'tools'}
+ >
+ )}
+
+
+ )
+
+ return (
+
+
+ exitState.pending ? (
+ Press {exitState.keyName} again to exit
+ ) : (
+
+ {onBack && }
+
+ {teammate.status === 'running' && onKill && (
+
+ )}
+ {teammate.status === 'running' && onForeground && (
+
+ )}
+
+ )
+ }
+ >
+ {/* Recent activities for running teammates */}
+ {teammate.status === 'running' &&
+ teammate.progress?.recentActivities &&
+ teammate.progress.recentActivities.length > 0 && (
+
+
+ Progress
+
+ {teammate.progress.recentActivities.map((activity, i) => (
+
+ {i === teammate.progress!.recentActivities!.length - 1
+ ? '› '
+ : ' '}
+ {renderToolActivity(activity, tools, theme)}
+
+ ))}
+
+ )}
+
+ {/* Prompt section */}
+
+
+ Prompt
+
+ {displayPrompt}
+
+
+ {/* Error details if failed */}
+ {teammate.status === 'failed' && teammate.error && (
+
+
+ Error
+
+
+ {teammate.error}
+
+
+ )}
+
+
+ )
}
diff --git a/src/components/tasks/RemoteSessionDetailDialog.tsx b/src/components/tasks/RemoteSessionDetailDialog.tsx
index f5435ead7..55c897fd9 100644
--- a/src/components/tasks/RemoteSessionDetailDialog.tsx
+++ b/src/components/tasks/RemoteSessionDetailDialog.tsx
@@ -1,41 +1,48 @@
-import { c as _c } from "react/compiler-runtime";
-import figures from 'figures';
-import React, { useMemo, useState } from 'react';
-import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.js';
-import type { ToolUseContext } from 'src/Tool.js';
-import type { DeepImmutable } from 'src/types/utils.js';
-import type { CommandResultDisplay } from '../../commands.js';
-import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js';
-import { useElapsedTime } from '../../hooks/useElapsedTime.js';
-import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
-import { Box, Link, Text } from '../../ink.js';
-import type { RemoteAgentTaskState } from '../../tasks/RemoteAgentTask/RemoteAgentTask.js';
-import { getRemoteTaskSessionUrl } from '../../tasks/RemoteAgentTask/RemoteAgentTask.js';
-import { AGENT_TOOL_NAME, LEGACY_AGENT_TOOL_NAME } from '../../tools/AgentTool/constants.js';
-import { ASK_USER_QUESTION_TOOL_NAME } from '../../tools/AskUserQuestionTool/prompt.js';
-import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../../tools/ExitPlanModeTool/constants.js';
-import { openBrowser } from '../../utils/browser.js';
-import { errorMessage } from '../../utils/errors.js';
-import { formatDuration, truncateToWidth } from '../../utils/format.js';
-import { toInternalMessages } from '../../utils/messages/mappers.js';
-import { EMPTY_LOOKUPS, normalizeMessages } from '../../utils/messages.js';
-import { plural } from '../../utils/stringUtils.js';
-import { teleportResumeCodeSession } from '../../utils/teleport.js';
-import { Select } from '../CustomSelect/select.js';
-import { Byline } from '../design-system/Byline.js';
-import { Dialog } from '../design-system/Dialog.js';
-import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
-import { Message } from '../Message.js';
-import { formatReviewStageCounts, RemoteSessionProgress } from './RemoteSessionProgress.js';
+import figures from 'figures'
+import React, { useMemo, useState } from 'react'
+import type { SDKMessage } from 'src/entrypoints/agentSdkTypes.js'
+import type { ToolUseContext } from 'src/Tool.js'
+import type { DeepImmutable } from 'src/types/utils.js'
+import type { CommandResultDisplay } from '../../commands.js'
+import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'
+import { useElapsedTime } from '../../hooks/useElapsedTime.js'
+import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
+import { Box, Link, Text } from '../../ink.js'
+import type { RemoteAgentTaskState } from '../../tasks/RemoteAgentTask/RemoteAgentTask.js'
+import { getRemoteTaskSessionUrl } from '../../tasks/RemoteAgentTask/RemoteAgentTask.js'
+import {
+ AGENT_TOOL_NAME,
+ LEGACY_AGENT_TOOL_NAME,
+} from '../../tools/AgentTool/constants.js'
+import { ASK_USER_QUESTION_TOOL_NAME } from '../../tools/AskUserQuestionTool/prompt.js'
+import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../../tools/ExitPlanModeTool/constants.js'
+import { openBrowser } from '../../utils/browser.js'
+import { errorMessage } from '../../utils/errors.js'
+import { formatDuration, truncateToWidth } from '../../utils/format.js'
+import { toInternalMessages } from '../../utils/messages/mappers.js'
+import { EMPTY_LOOKUPS, normalizeMessages } from '../../utils/messages.js'
+import { plural } from '../../utils/stringUtils.js'
+import { teleportResumeCodeSession } from '../../utils/teleport.js'
+import { Select } from '../CustomSelect/select.js'
+import { Byline } from '../design-system/Byline.js'
+import { Dialog } from '../design-system/Dialog.js'
+import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
+import { Message } from '../Message.js'
+import {
+ formatReviewStageCounts,
+ RemoteSessionProgress,
+} from './RemoteSessionProgress.js'
+
type Props = {
- session: DeepImmutable;
- toolUseContext: ToolUseContext;
- onDone: (result?: string, options?: {
- display?: CommandResultDisplay;
- }) => void;
- onBack?: () => void;
- onKill?: () => void;
-};
+ session: DeepImmutable
+ toolUseContext: ToolUseContext
+ onDone: (
+ result?: string,
+ options?: { display?: CommandResultDisplay },
+ ) => void
+ onBack?: () => void
+ onKill?: () => void
+}
// Compact one-line summary: tool name + first meaningful string arg.
// Lighter than tool.renderToolUseMessage (no registry lookup / schema parse).
@@ -44,746 +51,423 @@ type Props = {
export function formatToolUseSummary(name: string, input: unknown): string {
// plan_ready phase is only reached via ExitPlanMode tool
if (name === EXIT_PLAN_MODE_V2_TOOL_NAME) {
- return 'Review the plan in Claude Code on the web';
+ return 'Review the plan in Claude Code on the web'
}
- if (!input || typeof input !== 'object') return name;
+ if (!input || typeof input !== 'object') return name
// AskUserQuestion: show the question text as a CTA, not the tool name.
// Input shape is {questions: [{question, header, options}]}.
if (name === ASK_USER_QUESTION_TOOL_NAME && 'questions' in input) {
- const qs = input.questions;
+ const qs = input.questions
if (Array.isArray(qs) && qs[0] && typeof qs[0] === 'object') {
// Prefer question (full text) over header (max-12-char tag). header
// is a required schema field so checking it first would make the
// question fallback dead code.
- const q = 'question' in qs[0] && typeof qs[0].question === 'string' && qs[0].question ? qs[0].question : 'header' in qs[0] && typeof qs[0].header === 'string' ? qs[0].header : null;
+ const q =
+ 'question' in qs[0] &&
+ typeof qs[0].question === 'string' &&
+ qs[0].question
+ ? qs[0].question
+ : 'header' in qs[0] && typeof qs[0].header === 'string'
+ ? qs[0].header
+ : null
if (q) {
- const oneLine = q.replace(/\s+/g, ' ').trim();
- return `Answer in browser: ${truncateToWidth(oneLine, 50)}`;
+ const oneLine = q.replace(/\s+/g, ' ').trim()
+ return `Answer in browser: ${truncateToWidth(oneLine, 50)}`
}
}
}
for (const v of Object.values(input)) {
if (typeof v === 'string' && v.trim()) {
- const oneLine = v.replace(/\s+/g, ' ').trim();
- return `${name} ${truncateToWidth(oneLine, 60)}`;
+ const oneLine = v.replace(/\s+/g, ' ').trim()
+ return `${name} ${truncateToWidth(oneLine, 60)}`
}
}
- return name;
+ return name
}
+
const PHASE_LABEL = {
needs_input: 'input required',
- plan_ready: 'ready'
-} as const;
+ plan_ready: 'ready',
+} as const
+
const AGENT_VERB = {
needs_input: 'waiting',
- plan_ready: 'done'
-} as const;
-function UltraplanSessionDetail(t0) {
- const $ = _c(70);
- const {
- session,
- onDone,
- onBack,
- onKill
- } = t0;
- const running = session.status === "running" || session.status === "pending";
- const phase = session.ultraplanPhase;
- const statusText = running ? phase ? PHASE_LABEL[phase] : "running" : session.status;
- const elapsedTime = useElapsedTime(session.startTime, running, 1000, 0, session.endTime);
- let spawns = 0;
- let calls = 0;
- let lastBlock = null;
- for (const msg of session.log) {
- if (msg.type !== "assistant") {
- continue;
- }
- for (const block of msg.message.content) {
- if (block.type !== "tool_use") {
- continue;
- }
- calls++;
- lastBlock = block;
- if (block.name === AGENT_TOOL_NAME || block.name === LEGACY_AGENT_TOOL_NAME) {
- spawns++;
+ plan_ready: 'done',
+} as const
+
+function UltraplanSessionDetail({
+ session,
+ onDone,
+ onBack,
+ onKill,
+}: Omit): React.ReactNode {
+ const running = session.status === 'running' || session.status === 'pending'
+ const phase = session.ultraplanPhase
+ const statusText = running
+ ? phase
+ ? PHASE_LABEL[phase]
+ : 'running'
+ : session.status
+ const elapsedTime = useElapsedTime(
+ session.startTime,
+ running,
+ 1000,
+ 0,
+ session.endTime,
+ )
+
+ // Counts are eventually correct (lag ≤ poll interval). agentsWorking starts
+ // at 1 (the main session agent) and increments per subagent spawn. toolCalls
+ // is main-session only — subagent calls may not surface in this stream.
+ const { agentsWorking, toolCalls, lastToolCall } = useMemo(() => {
+ let spawns = 0
+ let calls = 0
+ let lastBlock: { name: string; input: unknown } | null = null
+ for (const msg of session.log) {
+ if (msg.type !== 'assistant') continue
+ for (const block of msg.message.content) {
+ if (block.type !== 'tool_use') continue
+ calls++
+ lastBlock = block
+ if (
+ block.name === AGENT_TOOL_NAME ||
+ block.name === LEGACY_AGENT_TOOL_NAME
+ ) {
+ spawns++
+ }
}
}
- }
- const t1 = 1 + spawns;
- let t2;
- if ($[0] !== lastBlock) {
- t2 = lastBlock ? formatToolUseSummary(lastBlock.name, lastBlock.input) : null;
- $[0] = lastBlock;
- $[1] = t2;
- } else {
- t2 = $[1];
- }
- let t3;
- if ($[2] !== calls || $[3] !== t1 || $[4] !== t2) {
- t3 = {
- agentsWorking: t1,
+ return {
+ agentsWorking: 1 + spawns,
toolCalls: calls,
- lastToolCall: t2
- };
- $[2] = calls;
- $[3] = t1;
- $[4] = t2;
- $[5] = t3;
- } else {
- t3 = $[5];
- }
- const {
- agentsWorking,
- toolCalls,
- lastToolCall
- } = t3;
- let t4;
- if ($[6] !== session.sessionId) {
- t4 = getRemoteTaskSessionUrl(session.sessionId);
- $[6] = session.sessionId;
- $[7] = t4;
- } else {
- t4 = $[7];
- }
- const sessionUrl = t4;
- let t5;
- if ($[8] !== onBack || $[9] !== onDone) {
- t5 = onBack ?? (() => onDone("Remote session details dismissed", {
- display: "system"
- }));
- $[8] = onBack;
- $[9] = onDone;
- $[10] = t5;
- } else {
- t5 = $[10];
- }
- const goBackOrClose = t5;
- const [confirmingStop, setConfirmingStop] = useState(false);
+ lastToolCall: lastBlock
+ ? formatToolUseSummary(lastBlock.name, lastBlock.input)
+ : null,
+ }
+ }, [session.log])
+
+ const sessionUrl = getRemoteTaskSessionUrl(session.sessionId)
+ const goBackOrClose =
+ onBack ??
+ (() => onDone('Remote session details dismissed', { display: 'system' }))
+ const [confirmingStop, setConfirmingStop] = useState(false)
+
if (confirmingStop) {
- let t6;
- if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
- t6 = () => setConfirmingStop(false);
- $[11] = t6;
- } else {
- t6 = $[11];
- }
- let t7;
- if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
- t7 = This will terminate the Claude Code on the web session.;
- $[12] = t7;
- } else {
- t7 = $[12];
- }
- let t8;
- if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
- t8 = {
- label: "Terminate session",
- value: "stop" as const
- };
- $[13] = t8;
- } else {
- t8 = $[13];
- }
- let t9;
- if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
- t9 = [t8, {
- label: "Back",
- value: "back" as const
- }];
- $[14] = t9;
- } else {
- t9 = $[14];
- }
- let t10;
- if ($[15] !== goBackOrClose || $[16] !== onKill) {
- t10 = {t7};
- $[15] = goBackOrClose;
- $[16] = onKill;
- $[17] = t10;
- } else {
- t10 = $[17];
- }
- return t10;
+ return (
+ setConfirmingStop(false)}
+ color="background"
+ >
+
+
+ This will terminate the Claude Code on the web session.
+
+
+
+ )
}
- const t6 = phase === "plan_ready" ? DIAMOND_FILLED : DIAMOND_OPEN;
- let t7;
- if ($[18] !== t6) {
- t7 = {t6}{" "};
- $[18] = t6;
- $[19] = t7;
- } else {
- t7 = $[19];
- }
- let t8;
- if ($[20] === Symbol.for("react.memo_cache_sentinel")) {
- t8 = ultraplan;
- $[20] = t8;
- } else {
- t8 = $[20];
- }
- let t9;
- if ($[21] !== elapsedTime || $[22] !== statusText) {
- t9 = {" \xB7 "}{elapsedTime}{" \xB7 "}{statusText};
- $[21] = elapsedTime;
- $[22] = statusText;
- $[23] = t9;
- } else {
- t9 = $[23];
- }
- let t10;
- if ($[24] !== t7 || $[25] !== t9) {
- t10 = {t7}{t8}{t9};
- $[24] = t7;
- $[25] = t9;
- $[26] = t10;
- } else {
- t10 = $[26];
- }
- let t11;
- if ($[27] !== phase) {
- t11 = phase === "plan_ready" && {figures.tick} ;
- $[27] = phase;
- $[28] = t11;
- } else {
- t11 = $[28];
- }
- let t12;
- if ($[29] !== agentsWorking) {
- t12 = plural(agentsWorking, "agent");
- $[29] = agentsWorking;
- $[30] = t12;
- } else {
- t12 = $[30];
- }
- const t13 = phase ? AGENT_VERB[phase] : "working";
- let t14;
- if ($[31] !== toolCalls) {
- t14 = plural(toolCalls, "call");
- $[31] = toolCalls;
- $[32] = t14;
- } else {
- t14 = $[32];
- }
- let t15;
- if ($[33] !== agentsWorking || $[34] !== t11 || $[35] !== t12 || $[36] !== t13 || $[37] !== t14 || $[38] !== toolCalls) {
- t15 = {t11}{agentsWorking} {t12}{" "}{t13} · {toolCalls} tool{" "}{t14};
- $[33] = agentsWorking;
- $[34] = t11;
- $[35] = t12;
- $[36] = t13;
- $[37] = t14;
- $[38] = toolCalls;
- $[39] = t15;
- } else {
- t15 = $[39];
- }
- let t16;
- if ($[40] !== lastToolCall) {
- t16 = lastToolCall && {lastToolCall};
- $[40] = lastToolCall;
- $[41] = t16;
- } else {
- t16 = $[41];
- }
- let t17;
- if ($[42] !== sessionUrl) {
- t17 = {sessionUrl};
- $[42] = sessionUrl;
- $[43] = t17;
- } else {
- t17 = $[43];
- }
- let t18;
- if ($[44] !== sessionUrl || $[45] !== t17) {
- t18 = {t17};
- $[44] = sessionUrl;
- $[45] = t17;
- $[46] = t18;
- } else {
- t18 = $[46];
- }
- let t19;
- if ($[47] === Symbol.for("react.memo_cache_sentinel")) {
- t19 = {
- label: "Review in Claude Code on the web",
- value: "open" as const
- };
- $[47] = t19;
- } else {
- t19 = $[47];
- }
- let t20;
- if ($[48] !== onKill || $[49] !== running) {
- t20 = onKill && running ? [{
- label: "Stop ultraplan",
- value: "stop" as const
- }] : [];
- $[48] = onKill;
- $[49] = running;
- $[50] = t20;
- } else {
- t20 = $[50];
- }
- let t21;
- if ($[51] === Symbol.for("react.memo_cache_sentinel")) {
- t21 = {
- label: "Back",
- value: "back" as const
- };
- $[51] = t21;
- } else {
- t21 = $[51];
- }
- let t22;
- if ($[52] !== t20) {
- t22 = [t19, ...t20, t21];
- $[52] = t20;
- $[53] = t22;
- } else {
- t22 = $[53];
- }
- let t23;
- if ($[54] !== goBackOrClose || $[55] !== onDone || $[56] !== sessionUrl) {
- t23 = v_0 => {
- switch (v_0) {
- case "open":
- {
- openBrowser(sessionUrl);
- onDone();
- return;
- }
- case "stop":
- {
- setConfirmingStop(true);
- return;
- }
- case "back":
- {
- goBackOrClose();
- return;
- }
+
+ return (
+
+
+ {phase === 'plan_ready' ? DIAMOND_FILLED : DIAMOND_OPEN}{' '}
+
+ ultraplan
+
+ {' · '}
+ {elapsedTime}
+ {' · '}
+ {statusText}
+
+
}
- };
- $[54] = goBackOrClose;
- $[55] = onDone;
- $[56] = sessionUrl;
- $[57] = t23;
- } else {
- t23 = $[57];
- }
- let t24;
- if ($[58] !== t22 || $[59] !== t23) {
- t24 = ;
- $[58] = t22;
- $[59] = t23;
- $[60] = t24;
- } else {
- t24 = $[60];
- }
- let t25;
- if ($[61] !== t15 || $[62] !== t16 || $[63] !== t18 || $[64] !== t24) {
- t25 = {t15}{t16}{t18}{t24};
- $[61] = t15;
- $[62] = t16;
- $[63] = t18;
- $[64] = t24;
- $[65] = t25;
- } else {
- t25 = $[65];
- }
- let t26;
- if ($[66] !== goBackOrClose || $[67] !== t10 || $[68] !== t25) {
- t26 = {t25};
- $[66] = goBackOrClose;
- $[67] = t10;
- $[68] = t25;
- $[69] = t26;
- } else {
- t26 = $[69];
- }
- return t26;
+ onCancel={goBackOrClose}
+ color="background"
+ >
+
+
+ {phase === 'plan_ready' && (
+ {figures.tick}
+ )}
+ {agentsWorking} {plural(agentsWorking, 'agent')}{' '}
+ {phase ? AGENT_VERB[phase] : 'working'} · {toolCalls} tool{' '}
+ {plural(toolCalls, 'call')}
+
+ {lastToolCall && {lastToolCall}}
+
+ {sessionUrl}
+
+
+
+ )
}
-const STAGES = ['finding', 'verifying', 'synthesizing'] as const;
+
+const STAGES = ['finding', 'verifying', 'synthesizing'] as const
const STAGE_LABELS: Record<(typeof STAGES)[number], string> = {
finding: 'Find',
verifying: 'Verify',
- synthesizing: 'Dedupe'
-};
+ synthesizing: 'Dedupe',
+}
// Setup → Find → Verify → Dedupe pipeline. Current stage in cloud teal,
// rest dim. When completed, all stages dim with a trailing green ✓. The
// "Setup" label shows before the orchestrator writes its first progress
// snapshot (container boot + repo clone), so the 0-found display doesn't
// look like a hung finder.
-function StagePipeline(t0) {
- const $ = _c(15);
- const {
- stage,
- completed,
- hasProgress
- } = t0;
- let t1;
- if ($[0] !== stage) {
- t1 = stage ? STAGES.indexOf(stage) : -1;
- $[0] = stage;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- const currentIdx = t1;
- const inSetup = !completed && !hasProgress;
- let t2;
- if ($[2] !== inSetup) {
- t2 = inSetup ? Setup : Setup;
- $[2] = inSetup;
- $[3] = t2;
- } else {
- t2 = $[3];
- }
- let t3;
- if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
- t3 = → ;
- $[4] = t3;
- } else {
- t3 = $[4];
- }
- let t4;
- if ($[5] !== completed || $[6] !== currentIdx || $[7] !== inSetup) {
- t4 = STAGES.map((s, i) => {
- const isCurrent = !completed && !inSetup && i === currentIdx;
- return {i > 0 && → }{isCurrent ? {STAGE_LABELS[s]} : {STAGE_LABELS[s]}};
- });
- $[5] = completed;
- $[6] = currentIdx;
- $[7] = inSetup;
- $[8] = t4;
- } else {
- t4 = $[8];
- }
- let t5;
- if ($[9] !== completed) {
- t5 = completed && ✓;
- $[9] = completed;
- $[10] = t5;
- } else {
- t5 = $[10];
- }
- let t6;
- if ($[11] !== t2 || $[12] !== t4 || $[13] !== t5) {
- t6 = {t2}{t3}{t4}{t5};
- $[11] = t2;
- $[12] = t4;
- $[13] = t5;
- $[14] = t6;
- } else {
- t6 = $[14];
- }
- return t6;
+function StagePipeline({
+ stage,
+ completed,
+ hasProgress,
+}: {
+ stage: 'finding' | 'verifying' | 'synthesizing' | undefined
+ completed: boolean
+ hasProgress: boolean
+}): React.ReactNode {
+ const currentIdx = stage ? STAGES.indexOf(stage) : -1
+ const inSetup = !completed && !hasProgress
+ return (
+
+ {inSetup ? (
+ Setup
+ ) : (
+ Setup
+ )}
+ →
+ {STAGES.map((s, i) => {
+ const isCurrent = !completed && !inSetup && i === currentIdx
+ return (
+
+ {i > 0 && → }
+ {isCurrent ? (
+ {STAGE_LABELS[s]}
+ ) : (
+ {STAGE_LABELS[s]}
+ )}
+
+ )
+ })}
+ {completed && ✓}
+
+ )
}
// Stage-appropriate counts line. Running-state formatting delegates to
// formatReviewStageCounts (shared with the pill) so the two views can't
// drift; completed state is dialog-specific (findings summary).
-function reviewCountsLine(session: DeepImmutable): string {
- const p = session.reviewProgress;
+function reviewCountsLine(
+ session: DeepImmutable,
+): string {
+ const p = session.reviewProgress
// No progress data — the orchestrator never wrote a snapshot. Don't
// claim "0 findings" when completed; we just don't know.
- if (!p) return session.status === 'completed' ? 'done' : 'setting up';
- const verified = p.bugsVerified;
- const refuted = p.bugsRefuted ?? 0;
+ if (!p) return session.status === 'completed' ? 'done' : 'setting up'
+ const verified = p.bugsVerified
+ const refuted = p.bugsRefuted ?? 0
if (session.status === 'completed') {
- const parts = [`${verified} ${plural(verified, 'finding')}`];
- if (refuted > 0) parts.push(`${refuted} refuted`);
- return parts.join(' · ');
+ const parts = [`${verified} ${plural(verified, 'finding')}`]
+ if (refuted > 0) parts.push(`${refuted} refuted`)
+ return parts.join(' · ')
}
- return formatReviewStageCounts(p.stage, p.bugsFound, verified, refuted);
+ return formatReviewStageCounts(p.stage, p.bugsFound, verified, refuted)
}
-type MenuAction = 'open' | 'stop' | 'back' | 'dismiss';
-function ReviewSessionDetail(t0) {
- const $ = _c(56);
- const {
- session,
- onDone,
- onBack,
- onKill
- } = t0;
- const completed = session.status === "completed";
- const running = session.status === "running" || session.status === "pending";
- const [confirmingStop, setConfirmingStop] = useState(false);
- const elapsedTime = useElapsedTime(session.startTime, running, 1000, 0, session.endTime);
- let t1;
- if ($[0] !== onDone) {
- t1 = () => onDone("Remote session details dismissed", {
- display: "system"
- });
- $[0] = onDone;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- const handleClose = t1;
- const goBackOrClose = onBack ?? handleClose;
- let t2;
- if ($[2] !== session.sessionId) {
- t2 = getRemoteTaskSessionUrl(session.sessionId);
- $[2] = session.sessionId;
- $[3] = t2;
- } else {
- t2 = $[3];
- }
- const sessionUrl = t2;
- const statusLabel = completed ? "ready" : running ? "running" : session.status;
+
+type MenuAction = 'open' | 'stop' | 'back' | 'dismiss'
+
+function ReviewSessionDetail({
+ session,
+ onDone,
+ onBack,
+ onKill,
+}: Omit): React.ReactNode {
+ const completed = session.status === 'completed'
+ const running = session.status === 'running' || session.status === 'pending'
+ const [confirmingStop, setConfirmingStop] = useState(false)
+
+ // useElapsedTime drives the 1Hz tick so the timer advances while the
+ // dialog is open — the previous inline elapsed-time calculation only
+ // re-rendered on session state changes (poll interval), which looked
+ // like the clock was stuck.
+ const elapsedTime = useElapsedTime(
+ session.startTime,
+ running,
+ 1000,
+ 0,
+ session.endTime,
+ )
+
+ const handleClose = () =>
+ onDone('Remote session details dismissed', { display: 'system' })
+ const goBackOrClose = onBack ?? handleClose
+
+ const sessionUrl = getRemoteTaskSessionUrl(session.sessionId)
+ const statusLabel = completed ? 'ready' : running ? 'running' : session.status
+
if (confirmingStop) {
- let t3;
- if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
- t3 = () => setConfirmingStop(false);
- $[4] = t3;
- } else {
- t3 = $[4];
- }
- let t4;
- if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
- t4 = This archives the remote session and stops local tracking. The review will not complete and any findings so far are discarded.;
- $[5] = t4;
- } else {
- t4 = $[5];
- }
- let t5;
- if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
- t5 = {
- label: "Stop ultrareview",
- value: "stop" as const
- };
- $[6] = t5;
- } else {
- t5 = $[6];
- }
- let t6;
- if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
- t6 = [t5, {
- label: "Back",
- value: "back" as const
- }];
- $[7] = t6;
- } else {
- t6 = $[7];
- }
- let t7;
- if ($[8] !== goBackOrClose || $[9] !== onKill) {
- t7 = {t4};
- $[8] = goBackOrClose;
- $[9] = onKill;
- $[10] = t7;
- } else {
- t7 = $[10];
- }
- return t7;
+ return (
+ setConfirmingStop(false)}
+ color="background"
+ >
+
+
+ This archives the remote session and stops local tracking. The
+ review will not complete and any findings so far are discarded.
+
+
+
+ )
}
- let t3;
- if ($[11] !== completed || $[12] !== onKill || $[13] !== running) {
- t3 = completed ? [{
- label: "Open in Claude Code on the web",
- value: "open"
- }, {
- label: "Dismiss",
- value: "dismiss"
- }] : [{
- label: "Open in Claude Code on the web",
- value: "open"
- }, ...(onKill && running ? [{
- label: "Stop ultrareview",
- value: "stop" as const
- }] : []), {
- label: "Back",
- value: "back"
- }];
- $[11] = completed;
- $[12] = onKill;
- $[13] = running;
- $[14] = t3;
- } else {
- t3 = $[14];
+
+ const options: { label: string; value: MenuAction }[] = completed
+ ? [
+ { label: 'Open in Claude Code on the web', value: 'open' },
+ { label: 'Dismiss', value: 'dismiss' },
+ ]
+ : [
+ { label: 'Open in Claude Code on the web', value: 'open' },
+ ...(onKill && running
+ ? [{ label: 'Stop ultrareview', value: 'stop' as const }]
+ : []),
+ { label: 'Back', value: 'back' },
+ ]
+
+ const handleSelect = (action: MenuAction) => {
+ switch (action) {
+ case 'open':
+ void openBrowser(sessionUrl)
+ onDone()
+ break
+ case 'stop':
+ setConfirmingStop(true)
+ break
+ case 'back':
+ goBackOrClose()
+ break
+ case 'dismiss':
+ handleClose()
+ break
+ }
}
- const options = t3;
- let t4;
- if ($[15] !== goBackOrClose || $[16] !== handleClose || $[17] !== onDone || $[18] !== sessionUrl) {
- t4 = action => {
- bb45: switch (action) {
- case "open":
- {
- openBrowser(sessionUrl);
- onDone();
- break bb45;
- }
- case "stop":
- {
- setConfirmingStop(true);
- break bb45;
- }
- case "back":
- {
- goBackOrClose();
- break bb45;
- }
- case "dismiss":
- {
- handleClose();
- }
+
+ return (
+
+
+ {completed ? DIAMOND_FILLED : DIAMOND_OPEN}{' '}
+
+ ultrareview
+
+ {' · '}
+ {elapsedTime}
+ {' · '}
+ {statusLabel}
+
+
}
- };
- $[15] = goBackOrClose;
- $[16] = handleClose;
- $[17] = onDone;
- $[18] = sessionUrl;
- $[19] = t4;
- } else {
- t4 = $[19];
- }
- const handleSelect = t4;
- const t5 = completed ? DIAMOND_FILLED : DIAMOND_OPEN;
- let t6;
- if ($[20] !== t5) {
- t6 = {t5}{" "};
- $[20] = t5;
- $[21] = t6;
- } else {
- t6 = $[21];
- }
- let t7;
- if ($[22] === Symbol.for("react.memo_cache_sentinel")) {
- t7 = ultrareview;
- $[22] = t7;
- } else {
- t7 = $[22];
- }
- let t8;
- if ($[23] !== elapsedTime || $[24] !== statusLabel) {
- t8 = {" \xB7 "}{elapsedTime}{" \xB7 "}{statusLabel};
- $[23] = elapsedTime;
- $[24] = statusLabel;
- $[25] = t8;
- } else {
- t8 = $[25];
- }
- let t9;
- if ($[26] !== t6 || $[27] !== t8) {
- t9 = {t6}{t7}{t8};
- $[26] = t6;
- $[27] = t8;
- $[28] = t9;
- } else {
- t9 = $[28];
- }
- const t10 = session.reviewProgress?.stage;
- const t11 = !!session.reviewProgress;
- let t12;
- if ($[29] !== completed || $[30] !== t10 || $[31] !== t11) {
- t12 = ;
- $[29] = completed;
- $[30] = t10;
- $[31] = t11;
- $[32] = t12;
- } else {
- t12 = $[32];
- }
- let t13;
- if ($[33] !== session) {
- t13 = reviewCountsLine(session);
- $[33] = session;
- $[34] = t13;
- } else {
- t13 = $[34];
- }
- let t14;
- if ($[35] !== t13) {
- t14 = {t13};
- $[35] = t13;
- $[36] = t14;
- } else {
- t14 = $[36];
- }
- let t15;
- if ($[37] !== sessionUrl) {
- t15 = {sessionUrl};
- $[37] = sessionUrl;
- $[38] = t15;
- } else {
- t15 = $[38];
- }
- let t16;
- if ($[39] !== sessionUrl || $[40] !== t15) {
- t16 = {t15};
- $[39] = sessionUrl;
- $[40] = t15;
- $[41] = t16;
- } else {
- t16 = $[41];
- }
- let t17;
- if ($[42] !== t14 || $[43] !== t16) {
- t17 = {t14}{t16};
- $[42] = t14;
- $[43] = t16;
- $[44] = t17;
- } else {
- t17 = $[44];
- }
- let t18;
- if ($[45] !== handleSelect || $[46] !== options) {
- t18 = ;
- $[45] = handleSelect;
- $[46] = options;
- $[47] = t18;
- } else {
- t18 = $[47];
- }
- let t19;
- if ($[48] !== t12 || $[49] !== t17 || $[50] !== t18) {
- t19 = {t12}{t17}{t18};
- $[48] = t12;
- $[49] = t17;
- $[50] = t18;
- $[51] = t19;
- } else {
- t19 = $[51];
- }
- let t20;
- if ($[52] !== goBackOrClose || $[53] !== t19 || $[54] !== t9) {
- t20 = {t19};
- $[52] = goBackOrClose;
- $[53] = t19;
- $[54] = t9;
- $[55] = t20;
- } else {
- t20 = $[55];
- }
- return t20;
-}
-function _temp(exitState) {
- return exitState.pending ? Press {exitState.keyName} again to exit : ;
+ onCancel={goBackOrClose}
+ color="background"
+ inputGuide={exitState =>
+ exitState.pending ? (
+ Press {exitState.keyName} again to exit
+ ) : (
+
+
+
+
+ )
+ }
+ >
+
+
+
+
+ {reviewCountsLine(session)}
+
+ {sessionUrl}
+
+
+
+
+
+
+ )
}
+
export function RemoteSessionDetailDialog({
session,
toolUseContext,
onDone,
onBack,
- onKill
+ onKill,
}: Props): React.ReactNode {
- const [isTeleporting, setIsTeleporting] = useState(false);
- const [teleportError, setTeleportError] = useState(null);
+ const [isTeleporting, setIsTeleporting] = useState(false)
+ const [teleportError, setTeleportError] = useState(null)
// Get last few messages from remote session for display.
// Scan all messages (not just the last 3 raw entries) because the tail of
@@ -791,74 +475,119 @@ export function RemoteSessionDetailDialog({
// Placed before the early returns so hook call order is stable (Rules of Hooks).
// Ultraplan/review sessions never read this — skip the normalize work for them.
const lastMessages = useMemo(() => {
- if (session.isUltraplan || session.isRemoteReview) return [];
- return normalizeMessages(toInternalMessages(session.log as SDKMessage[])).filter(_ => _.type !== 'progress').slice(-3);
- }, [session]);
+ if (session.isUltraplan || session.isRemoteReview) return []
+ return normalizeMessages(toInternalMessages(session.log as SDKMessage[]))
+ .filter(_ => _.type !== 'progress')
+ .slice(-3)
+ }, [session])
+
if (session.isUltraplan) {
- return ;
+ return (
+
+ )
}
// Review sessions get the stage-pipeline view; everything else keeps the
// generic label/value + recent-messages dialog below.
if (session.isRemoteReview) {
- return ;
+ return (
+
+ )
}
- const handleClose = () => onDone('Remote session details dismissed', {
- display: 'system'
- });
+
+ const handleClose = () =>
+ onDone('Remote session details dismissed', { display: 'system' })
// Component-specific shortcuts shown in UI hints (t=teleport, space=dismiss,
// left=back). These are state-dependent actions, not standard dialog keybindings.
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === ' ') {
- e.preventDefault();
- onDone('Remote session details dismissed', {
- display: 'system'
- });
+ e.preventDefault()
+ onDone('Remote session details dismissed', { display: 'system' })
} else if (e.key === 'left' && onBack) {
- e.preventDefault();
- onBack();
+ e.preventDefault()
+ onBack()
} else if (e.key === 't' && !isTeleporting) {
- e.preventDefault();
- void handleTeleport();
+ e.preventDefault()
+ void handleTeleport()
} else if (e.key === 'return') {
- e.preventDefault();
- handleClose();
+ e.preventDefault()
+ handleClose()
}
- };
+ }
// Handle teleporting to remote session
async function handleTeleport(): Promise {
- setIsTeleporting(true);
- setTeleportError(null);
+ setIsTeleporting(true)
+ setTeleportError(null)
+
try {
- await teleportResumeCodeSession(session.sessionId);
+ await teleportResumeCodeSession(session.sessionId)
} catch (err) {
- setTeleportError(errorMessage(err));
+ setTeleportError(errorMessage(err))
} finally {
- setIsTeleporting(false);
+ setIsTeleporting(false)
}
}
// Truncate title if too long (for display purposes)
- const displayTitle = truncateToWidth(session.title, 50);
+ const displayTitle = truncateToWidth(session.title, 50)
// Map TaskStatus to display status (handle 'pending')
- const displayStatus = session.status === 'pending' ? 'starting' : session.status;
- return
- exitState.pending ? Press {exitState.keyName} again to exit :
+ const displayStatus =
+ session.status === 'pending' ? 'starting' : session.status
+
+ return (
+
+
+ exitState.pending ? (
+ Press {exitState.keyName} again to exit
+ ) : (
+
{onBack && }
- {!isTeleporting && }
- }>
+ {!isTeleporting && (
+
+ )}
+
+ )
+ }
+ >
Status:{' '}
- {displayStatus === 'running' || displayStatus === 'starting' ? {displayStatus} : displayStatus === 'completed' ? {displayStatus} : {displayStatus}}
+ {displayStatus === 'running' || displayStatus === 'starting' ? (
+ {displayStatus}
+ ) : displayStatus === 'completed' ? (
+ {displayStatus}
+ ) : (
+ {displayStatus}
+ )}
Runtime:{' '}
- {formatDuration((session.endTime ?? Date.now()) - session.startTime)}
+ {formatDuration(
+ (session.endTime ?? Date.now()) - session.startTime,
+ )}
Title: {displayTitle}
@@ -876,12 +605,30 @@ export function RemoteSessionDetailDialog({
{/* Remote session messages section */}
- {session.log.length > 0 &&
+ {session.log.length > 0 && (
+
Recent messages:
- {lastMessages.map((msg, i) => 0} tools={toolUseContext.options.tools} commands={toolUseContext.options.commands} verbose={toolUseContext.options.verbose} inProgressToolUseIDs={new Set()} progressMessagesForMessage={[]} shouldAnimate={false} shouldShowDot={false} style="condensed" isTranscriptMode={false} isStatic={true} />)}
+ {lastMessages.map((msg, i) => (
+ 0}
+ tools={toolUseContext.options.tools}
+ commands={toolUseContext.options.commands}
+ verbose={toolUseContext.options.verbose}
+ inProgressToolUseIDs={new Set()}
+ progressMessagesForMessage={[]}
+ shouldAnimate={false}
+ shouldShowDot={false}
+ style="condensed"
+ isTranscriptMode={false}
+ isStatic={true}
+ />
+ ))}
@@ -889,15 +636,21 @@ export function RemoteSessionDetailDialog({
messages
- }
+
+ )}
{/* Teleport error message */}
- {teleportError &&
+ {teleportError && (
+
Teleport failed: {teleportError}
- }
+
+ )}
{/* Teleporting status */}
- {isTeleporting && Teleporting to session…}
+ {isTeleporting && (
+ Teleporting to session…
+ )}
- ;
+
+ )
}
diff --git a/src/components/tasks/RemoteSessionProgress.tsx b/src/components/tasks/RemoteSessionProgress.tsx
index 2da0140a5..c1711cd8a 100644
--- a/src/components/tasks/RemoteSessionProgress.tsx
+++ b/src/components/tasks/RemoteSessionProgress.tsx
@@ -1,14 +1,17 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { useRef } from 'react';
-import type { RemoteAgentTaskState } from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js';
-import type { DeepImmutable } from 'src/types/utils.js';
-import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js';
-import { useSettings } from '../../hooks/useSettings.js';
-import { Text, useAnimationFrame } from '../../ink.js';
-import { count } from '../../utils/array.js';
-import { getRainbowColor } from '../../utils/thinking.js';
-const TICK_MS = 80;
-type ReviewStage = NonNullable['stage']>;
+import React, { useRef } from 'react'
+import type { RemoteAgentTaskState } from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js'
+import type { DeepImmutable } from 'src/types/utils.js'
+import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'
+import { useSettings } from '../../hooks/useSettings.js'
+import { Text, useAnimationFrame } from '../../ink.js'
+import { count } from '../../utils/array.js'
+import { getRainbowColor } from '../../utils/thinking.js'
+
+const TICK_MS = 80
+
+type ReviewStage = NonNullable<
+ NonNullable['stage']
+>
/**
* Stage-appropriate counts line for a running review. Shared between the
@@ -19,52 +22,48 @@ type ReviewStage = NonNullable 0) parts.push(`${refuted} refuted`);
- parts.push('deduping');
- return parts.join(' · ');
+ const parts = [`${verified} verified`]
+ if (refuted > 0) parts.push(`${refuted} refuted`)
+ parts.push('deduping')
+ return parts.join(' · ')
}
if (stage === 'verifying') {
- const parts = [`${found} found`, `${verified} verified`];
- if (refuted > 0) parts.push(`${refuted} refuted`);
- return parts.join(' · ');
+ const parts = [`${found} found`, `${verified} verified`]
+ if (refuted > 0) parts.push(`${refuted} refuted`)
+ return parts.join(' · ')
}
// stage === 'finding'
- return found > 0 ? `${found} found` : 'finding';
+ return found > 0 ? `${found} found` : 'finding'
}
// Per-character rainbow gradient, same treatment as the ultraplan keyword.
// The phase offset lets the gradient cycle — so the colors sweep along the
// text on each animation frame instead of being static.
-function RainbowText(t0) {
- const $ = _c(5);
- const {
- text,
- phase: t1
- } = t0;
- const phase = t1 === undefined ? 0 : t1;
- let t2;
- if ($[0] !== text) {
- t2 = [...text];
- $[0] = text;
- $[1] = t2;
- } else {
- t2 = $[1];
- }
- let t3;
- if ($[2] !== phase || $[3] !== t2) {
- t3 = <>{t2.map((ch, i) => {ch})}>;
- $[2] = phase;
- $[3] = t2;
- $[4] = t3;
- } else {
- t3 = $[4];
- }
- return t3;
+function RainbowText({
+ text,
+ phase = 0,
+}: {
+ text: string
+ phase?: number
+}): React.ReactNode {
+ return (
+ <>
+ {[...text].map((ch, i) => (
+
+ {ch}
+
+ ))}
+ >
+ )
}
// Smooth-tick a count toward target, +1 per frame. Same pattern as the
@@ -74,169 +73,129 @@ function RainbowText(t0) {
// the clock is frozen), bypass the tick and jump straight to target —
// otherwise a frozen `time` would leave the ref stuck at its init value.
function useSmoothCount(target: number, time: number, snap: boolean): number {
- const displayed = useRef(target);
- const lastTick = useRef(time);
+ const displayed = useRef(target)
+ const lastTick = useRef(time)
if (snap || target < displayed.current) {
- displayed.current = target;
+ displayed.current = target
} else if (target > displayed.current && time !== lastTick.current) {
- displayed.current += 1;
- lastTick.current = time;
+ displayed.current += 1
+ lastTick.current = time
}
- return displayed.current;
+ return displayed.current
}
-function ReviewRainbowLine(t0) {
- const $ = _c(15);
- const {
- session
- } = t0;
- const settings = useSettings();
- const reducedMotion = settings.prefersReducedMotion ?? false;
- const p = session.reviewProgress;
- const running = session.status === "running";
- const [, time] = useAnimationFrame(running && !reducedMotion ? TICK_MS : null);
- const targetFound = p?.bugsFound ?? 0;
- const targetVerified = p?.bugsVerified ?? 0;
- const targetRefuted = p?.bugsRefuted ?? 0;
- const snap = reducedMotion || !running;
- const found = useSmoothCount(targetFound, time, snap);
- const verified = useSmoothCount(targetVerified, time, snap);
- const refuted = useSmoothCount(targetRefuted, time, snap);
- const phase = Math.floor(time / (TICK_MS * 3)) % 7;
- if (session.status === "completed") {
- let t1;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = <>{DIAMOND_FILLED} ready · shift+↓ to view>;
- $[0] = t1;
- } else {
- t1 = $[0];
- }
- return t1;
+
+function ReviewRainbowLine({
+ session,
+}: {
+ session: DeepImmutable
+}): React.ReactNode {
+ const settings = useSettings()
+ const reducedMotion = settings.prefersReducedMotion ?? false
+ const p = session.reviewProgress
+ const running = session.status === 'running'
+ // Animation clock runs only while running — completed/failed are static.
+ // Disabled entirely when the user prefers reduced motion.
+ //
+ // The ref is intentionally discarded: this component is rendered inside
+ // wrappers (BackgroundTasksDialog, RemoteSessionDetailDialog), and
+ // Ink can't nest inside . Dropping the ref means
+ // useTerminalViewport's isVisible stays true, so the clock ticks even when
+ // scrolled off-screen — acceptable for a single 30-char line.
+ const [, time] = useAnimationFrame(running && !reducedMotion ? TICK_MS : null)
+
+ const targetFound = p?.bugsFound ?? 0
+ const targetVerified = p?.bugsVerified ?? 0
+ const targetRefuted = p?.bugsRefuted ?? 0
+ // snap when the clock isn't advancing (reduced motion, or not running) —
+ // useAnimationFrame(null) freezes `time` at its mount value, which would
+ // leave the tick-gate permanently false.
+ const snap = reducedMotion || !running
+ const found = useSmoothCount(targetFound, time, snap)
+ const verified = useSmoothCount(targetVerified, time, snap)
+ const refuted = useSmoothCount(targetRefuted, time, snap)
+
+ // Phase advances every 3 ticks so the gradient sweep is visible but
+ // not frantic. Modulo keeps it in the 7-color cycle.
+ const phase = Math.floor(time / (TICK_MS * 3)) % 7
+
+ // ◇ open diamond while running (teal, matches cloud-session accent), ◆
+ // filled when terminal. Rainbow is scoped to the word `ultrareview` only —
+ // per design feedback, "there is a limit to the glittering rainbow".
+ // Counts stay dimColor.
+ if (session.status === 'completed') {
+ return (
+ <>
+ {DIAMOND_FILLED}
+
+ ready · shift+↓ to view
+ >
+ )
}
- if (session.status === "failed") {
- let t1;
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = <>{DIAMOND_FILLED} {" \xB7 "}error>;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- return t1;
+ if (session.status === 'failed') {
+ return (
+ <>
+ {DIAMOND_FILLED}
+
+
+ {' · '}
+ error
+
+ >
+ )
}
- let t1;
- if ($[2] !== found || $[3] !== p || $[4] !== refuted || $[5] !== verified) {
- t1 = !p ? "setting up" : formatReviewStageCounts(p.stage, found, verified, refuted);
- $[2] = found;
- $[3] = p;
- $[4] = refuted;
- $[5] = verified;
- $[6] = t1;
- } else {
- t1 = $[6];
- }
- const tail = t1;
- let t2;
- if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
- t2 = {DIAMOND_OPEN} ;
- $[7] = t2;
- } else {
- t2 = $[7];
- }
- const t3 = running ? phase : 0;
- let t4;
- if ($[8] !== t3) {
- t4 = ;
- $[8] = t3;
- $[9] = t4;
- } else {
- t4 = $[9];
- }
- let t5;
- if ($[10] !== tail) {
- t5 = · {tail};
- $[10] = tail;
- $[11] = t5;
- } else {
- t5 = $[11];
- }
- let t6;
- if ($[12] !== t4 || $[13] !== t5) {
- t6 = <>{t2}{t4}{t5}>;
- $[12] = t4;
- $[13] = t5;
- $[14] = t6;
- } else {
- t6 = $[14];
- }
- return t6;
+
+ // The !p branch ("setting up") covers the window before the orchestrator
+ // writes its first progress snapshot — container boot + repo clone can
+ // take 1-3 min, during which "0 found" looked hung.
+ const tail = !p
+ ? 'setting up'
+ : formatReviewStageCounts(p.stage, found, verified, refuted)
+ return (
+ <>
+ {DIAMOND_OPEN}
+
+ · {tail}
+ >
+ )
}
-export function RemoteSessionProgress(t0) {
- const $ = _c(11);
- const {
- session
- } = t0;
+
+export function RemoteSessionProgress({
+ session,
+}: {
+ session: DeepImmutable
+}): React.ReactNode {
+ // Lite-review: rainbow gradient over the full line, ultraplan-style.
+ // BackgroundTask.tsx delegates the whole wrapper here so the
+ // gradient spans the title, not just the trailing status.
if (session.isRemoteReview) {
- let t1;
- if ($[0] !== session) {
- t1 = ;
- $[0] = session;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- return t1;
+ return
}
- if (session.status === "completed") {
- let t1;
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = done;
- $[2] = t1;
- } else {
- t1 = $[2];
- }
- return t1;
+
+ if (session.status === 'completed') {
+ return (
+
+ done
+
+ )
}
- if (session.status === "failed") {
- let t1;
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = error;
- $[3] = t1;
- } else {
- t1 = $[3];
- }
- return t1;
+
+ if (session.status === 'failed') {
+ return (
+
+ error
+
+ )
}
+
if (!session.todoList.length) {
- let t1;
- if ($[4] !== session.status) {
- t1 = {session.status}…;
- $[4] = session.status;
- $[5] = t1;
- } else {
- t1 = $[5];
- }
- return t1;
+ return {session.status}…
}
- let t1;
- if ($[6] !== session.todoList) {
- t1 = count(session.todoList, _temp);
- $[6] = session.todoList;
- $[7] = t1;
- } else {
- t1 = $[7];
- }
- const completed = t1;
- const total = session.todoList.length;
- let t2;
- if ($[8] !== completed || $[9] !== total) {
- t2 = {completed}/{total};
- $[8] = completed;
- $[9] = total;
- $[10] = t2;
- } else {
- t2 = $[10];
- }
- return t2;
-}
-function _temp(_) {
- return _.status === "completed";
+
+ const completed = count(session.todoList, _ => _.status === 'completed')
+ const total = session.todoList.length
+ return (
+
+ {completed}/{total}
+
+ )
}
diff --git a/src/components/tasks/ShellDetailDialog.tsx b/src/components/tasks/ShellDetailDialog.tsx
index d42472c31..a81bafc8b 100644
--- a/src/components/tasks/ShellDetailDialog.tsx
+++ b/src/components/tasks/ShellDetailDialog.tsx
@@ -1,403 +1,247 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { Suspense, use, useDeferredValue, useEffect, useState } from 'react';
-import type { DeepImmutable } from 'src/types/utils.js';
-import type { CommandResultDisplay } from '../../commands.js';
-import { useTerminalSize } from '../../hooks/useTerminalSize.js';
-import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
-import { Box, Text } from '../../ink.js';
-import { useKeybindings } from '../../keybindings/useKeybinding.js';
-import type { LocalShellTaskState } from '../../tasks/LocalShellTask/guards.js';
-import { formatDuration, formatFileSize, truncateToWidth } from '../../utils/format.js';
-import { tailFile } from '../../utils/fsOperations.js';
-import { getTaskOutputPath } from '../../utils/task/diskOutput.js';
-import { Byline } from '../design-system/Byline.js';
-import { Dialog } from '../design-system/Dialog.js';
-import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
+import React, {
+ Suspense,
+ use,
+ useDeferredValue,
+ useEffect,
+ useState,
+} from 'react'
+import type { DeepImmutable } from 'src/types/utils.js'
+import type { CommandResultDisplay } from '../../commands.js'
+import { useTerminalSize } from '../../hooks/useTerminalSize.js'
+import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
+import { Box, Text } from '../../ink.js'
+import { useKeybindings } from '../../keybindings/useKeybinding.js'
+import type { LocalShellTaskState } from '../../tasks/LocalShellTask/guards.js'
+import {
+ formatDuration,
+ formatFileSize,
+ truncateToWidth,
+} from '../../utils/format.js'
+import { tailFile } from '../../utils/fsOperations.js'
+import { getTaskOutputPath } from '../../utils/task/diskOutput.js'
+import { Byline } from '../design-system/Byline.js'
+import { Dialog } from '../design-system/Dialog.js'
+import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
+
type Props = {
- shell: DeepImmutable;
- onDone: (result?: string, options?: {
- display?: CommandResultDisplay;
- }) => void;
- onKillShell?: () => void;
- onBack?: () => void;
-};
-const SHELL_DETAIL_TAIL_BYTES = 8192;
+ shell: DeepImmutable
+ onDone: (
+ result?: string,
+ options?: { display?: CommandResultDisplay },
+ ) => void
+ onKillShell?: () => void
+ onBack?: () => void
+}
+
+const SHELL_DETAIL_TAIL_BYTES = 8192
+
type TaskOutputResult = {
- content: string;
- bytesTotal: number;
-};
+ content: string
+ bytesTotal: number
+}
/**
* Read the tail of the task output file. Only reads the last few KB,
* not the entire file.
*/
-async function getTaskOutput(shell: DeepImmutable): Promise {
- const path = getTaskOutputPath(shell.id);
+async function getTaskOutput(
+ shell: DeepImmutable,
+): Promise {
+ const path = getTaskOutputPath(shell.id)
try {
- const result = await tailFile(path, SHELL_DETAIL_TAIL_BYTES);
- return {
- content: result.content,
- bytesTotal: result.bytesTotal
- };
+ const result = await tailFile(path, SHELL_DETAIL_TAIL_BYTES)
+ return { content: result.content, bytesTotal: result.bytesTotal }
} catch {
- return {
- content: '',
- bytesTotal: 0
- };
+ return { content: '', bytesTotal: 0 }
}
}
-export function ShellDetailDialog(t0) {
- const $ = _c(57);
- const {
- shell,
- onDone,
- onKillShell,
- onBack
- } = t0;
- const {
- columns
- } = useTerminalSize();
- let t1;
- if ($[0] !== shell) {
- t1 = () => getTaskOutput(shell);
- $[0] = shell;
- $[1] = t1;
- } else {
- t1 = $[1];
+
+export function ShellDetailDialog({
+ shell,
+ onDone,
+ onKillShell,
+ onBack,
+}: Props): React.ReactNode {
+ const { columns } = useTerminalSize()
+
+ // Promise created in initializer (not during render). For running shells,
+ // the effect timer replaces it periodically to pick up new output.
+ // useDeferredValue keeps showing the previous output while the new promise
+ // resolves, preventing the Suspense fallback from flickering.
+ const [outputPromise, setOutputPromise] = useState>(
+ () => getTaskOutput(shell),
+ )
+ const deferredOutputPromise = useDeferredValue(outputPromise)
+
+ useEffect(() => {
+ if (shell.status !== 'running') {
+ return
+ }
+ const timer = setInterval(
+ (setOutputPromise, shell) => setOutputPromise(getTaskOutput(shell)),
+ 1000,
+ setOutputPromise,
+ shell,
+ )
+ return () => clearInterval(timer)
+ }, [shell.id, shell.status])
+
+ // Handle standard close action
+ const handleClose = () =>
+ onDone('Shell details dismissed', { display: 'system' })
+
+ // Handle additional close actions beyond Dialog's built-in Esc handler
+ useKeybindings(
+ {
+ 'confirm:yes': handleClose,
+ },
+ { context: 'Confirmation' },
+ )
+
+ // Handle dialog-specific keys
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === ' ') {
+ e.preventDefault()
+ onDone('Shell details dismissed', { display: 'system' })
+ } else if (e.key === 'left' && onBack) {
+ e.preventDefault()
+ onBack()
+ } else if (e.key === 'x' && shell.status === 'running' && onKillShell) {
+ e.preventDefault()
+ onKillShell()
+ }
}
- const [outputPromise, setOutputPromise] = useState(t1);
- const deferredOutputPromise = useDeferredValue(outputPromise);
- let t2;
- if ($[2] !== shell) {
- t2 = () => {
- if (shell.status !== "running") {
- return;
- }
- const timer = setInterval(_temp, 1000, setOutputPromise, shell);
- return () => clearInterval(timer);
- };
- $[2] = shell;
- $[3] = t2;
- } else {
- t2 = $[3];
- }
- let t3;
- if ($[4] !== shell.id || $[5] !== shell.status) {
- t3 = [shell.id, shell.status];
- $[4] = shell.id;
- $[5] = shell.status;
- $[6] = t3;
- } else {
- t3 = $[6];
- }
- useEffect(t2, t3);
- let t4;
- if ($[7] !== onDone) {
- t4 = () => onDone("Shell details dismissed", {
- display: "system"
- });
- $[7] = onDone;
- $[8] = t4;
- } else {
- t4 = $[8];
- }
- const handleClose = t4;
- let t5;
- if ($[9] !== handleClose) {
- t5 = {
- "confirm:yes": handleClose
- };
- $[9] = handleClose;
- $[10] = t5;
- } else {
- t5 = $[10];
- }
- let t6;
- if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
- t6 = {
- context: "Confirmation"
- };
- $[11] = t6;
- } else {
- t6 = $[11];
- }
- useKeybindings(t5, t6);
- let t7;
- if ($[12] !== onBack || $[13] !== onDone || $[14] !== onKillShell || $[15] !== shell.status) {
- t7 = e => {
- if (e.key === " ") {
- e.preventDefault();
- onDone("Shell details dismissed", {
- display: "system"
- });
- } else {
- if (e.key === "left" && onBack) {
- e.preventDefault();
- onBack();
- } else {
- if (e.key === "x" && shell.status === "running" && onKillShell) {
- e.preventDefault();
- onKillShell();
- }
+
+ // Truncate command if too long (for display purposes)
+ const isMonitor = shell.kind === 'monitor'
+ const displayCommand = truncateToWidth(shell.command, 280)
+
+ return (
+
+
+ exitState.pending ? (
+ Press {exitState.keyName} again to exit
+ ) : (
+
+ {onBack && }
+
+ {shell.status === 'running' && onKillShell && (
+
+ )}
+
+ )
}
- }
- };
- $[12] = onBack;
- $[13] = onDone;
- $[14] = onKillShell;
- $[15] = shell.status;
- $[16] = t7;
- } else {
- t7 = $[16];
- }
- const handleKeyDown = t7;
- const isMonitor = shell.kind === "monitor";
- let t8;
- if ($[17] !== shell.command) {
- t8 = truncateToWidth(shell.command, 280);
- $[17] = shell.command;
- $[18] = t8;
- } else {
- t8 = $[18];
- }
- const displayCommand = t8;
- const t9 = isMonitor ? "Monitor details" : "Shell details";
- let t10;
- if ($[19] !== onBack || $[20] !== onKillShell || $[21] !== shell.status) {
- t10 = exitState => exitState.pending ? Press {exitState.keyName} again to exit : {onBack && }{shell.status === "running" && onKillShell && };
- $[19] = onBack;
- $[20] = onKillShell;
- $[21] = shell.status;
- $[22] = t10;
- } else {
- t10 = $[22];
- }
- let t11;
- if ($[23] === Symbol.for("react.memo_cache_sentinel")) {
- t11 = Status:;
- $[23] = t11;
- } else {
- t11 = $[23];
- }
- let t12;
- if ($[24] !== shell.result || $[25] !== shell.status) {
- t12 = {t11}{" "}{shell.status === "running" ? {shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`} : shell.status === "completed" ? {shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`} : {shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`}};
- $[24] = shell.result;
- $[25] = shell.status;
- $[26] = t12;
- } else {
- t12 = $[26];
- }
- let t13;
- if ($[27] === Symbol.for("react.memo_cache_sentinel")) {
- t13 = Runtime:;
- $[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 = {t13}{" "}{t16};
- $[32] = t16;
- $[33] = t17;
- } else {
- t17 = $[33];
- }
- const t18 = isMonitor ? "Script:" : "Command:";
- let t19;
- if ($[34] !== t18) {
- t19 = {t18};
- $[34] = t18;
- $[35] = t19;
- } else {
- t19 = $[35];
- }
- let t20;
- if ($[36] !== displayCommand || $[37] !== t19) {
- t20 = {t19}{" "}{displayCommand};
- $[36] = displayCommand;
- $[37] = t19;
- $[38] = t20;
- } else {
- t20 = $[38];
- }
- let t21;
- if ($[39] !== t12 || $[40] !== t17 || $[41] !== t20) {
- t21 = {t12}{t17}{t20};
- $[39] = t12;
- $[40] = t17;
- $[41] = t20;
- $[42] = t21;
- } else {
- t21 = $[42];
- }
- let t22;
- if ($[43] === Symbol.for("react.memo_cache_sentinel")) {
- t22 = Output:;
- $[43] = t22;
- } else {
- t22 = $[43];
- }
- let t23;
- if ($[44] === Symbol.for("react.memo_cache_sentinel")) {
- t23 = Loading output…;
- $[44] = t23;
- } else {
- t23 = $[44];
- }
- let t24;
- if ($[45] !== columns || $[46] !== deferredOutputPromise) {
- t24 = {t22};
- $[45] = columns;
- $[46] = deferredOutputPromise;
- $[47] = t24;
- } else {
- t24 = $[47];
- }
- let t25;
- if ($[48] !== handleClose || $[49] !== t10 || $[50] !== t21 || $[51] !== t24 || $[52] !== t9) {
- t25 = {t21}{t24};
- $[48] = handleClose;
- $[49] = t10;
- $[50] = t21;
- $[51] = t24;
- $[52] = t9;
- $[53] = t25;
- } else {
- t25 = $[53];
- }
- let t26;
- if ($[54] !== handleKeyDown || $[55] !== t25) {
- t26 = {t25};
- $[54] = handleKeyDown;
- $[55] = t25;
- $[56] = t26;
- } else {
- t26 = $[56];
- }
- return t26;
-}
-function _temp(setOutputPromise_0, shell_0) {
- return setOutputPromise_0(getTaskOutput(shell_0));
+ >
+
+
+ Status:{' '}
+ {shell.status === 'running' ? (
+
+ {shell.status}
+ {shell.result?.code !== undefined &&
+ ` (exit code: ${shell.result.code})`}
+
+ ) : shell.status === 'completed' ? (
+
+ {shell.status}
+ {shell.result?.code !== undefined &&
+ ` (exit code: ${shell.result.code})`}
+
+ ) : (
+
+ {shell.status}
+ {shell.result?.code !== undefined &&
+ ` (exit code: ${shell.result.code})`}
+
+ )}
+
+
+ Runtime:{' '}
+ {formatDuration((shell.endTime ?? Date.now()) - shell.startTime)}
+
+
+ {isMonitor ? 'Script:' : 'Command:'}{' '}
+ {displayCommand}
+
+
+
+
+ Output:
+ Loading output…}>
+
+
+
+
+
+ )
}
+
type ShellOutputContentProps = {
- outputPromise: Promise;
- columns: number;
-};
-function ShellOutputContent(t0) {
- const $ = _c(19);
- const {
- outputPromise,
- columns
- } = t0;
- const {
- content,
- bytesTotal
- } = use(outputPromise) as any;
+ outputPromise: Promise
+ columns: number
+}
+
+function ShellOutputContent({
+ outputPromise,
+ columns,
+}: ShellOutputContentProps): React.ReactNode {
+ const { content, bytesTotal } = use(outputPromise)
+
if (!content) {
- let t1;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = No output available;
- $[0] = t1;
- } else {
- t1 = $[0];
- }
- return t1;
+ return No output available
}
- let isIncomplete;
- let rendered;
- if ($[1] !== bytesTotal || $[2] !== content) {
- const starts = [];
- let pos = content.length;
- for (let i = 0; i < 10 && pos > 0; i++) {
- const prev = content.lastIndexOf("\n", pos - 1);
- starts.push(prev + 1);
- pos = prev;
- }
- starts.reverse();
- isIncomplete = bytesTotal > content.length;
- rendered = [];
- for (let i_0 = 0; i_0 < starts.length; i_0++) {
- const start = starts[i_0];
- const end = i_0 < starts.length - 1 ? starts[i_0 + 1] - 1 : content.length;
- const line = content.slice(start, end);
- if (line) {
- rendered.push(line);
- }
- }
- $[1] = bytesTotal;
- $[2] = content;
- $[3] = isIncomplete;
- $[4] = rendered;
- } else {
- isIncomplete = $[3];
- rendered = $[4];
+
+ // Find last 10 line boundaries via lastIndexOf
+ const starts: number[] = []
+ let pos = content.length
+ for (let i = 0; i < 10 && pos > 0; i++) {
+ const prev = content.lastIndexOf('\n', pos - 1)
+ starts.push(prev + 1)
+ pos = prev
}
- const t1 = columns - 6;
- let t2;
- if ($[5] !== rendered) {
- t2 = rendered.map(_temp2);
- $[5] = rendered;
- $[6] = t2;
- } else {
- t2 = $[6];
+ starts.reverse()
+ const isIncomplete = bytesTotal > content.length
+
+ // Build lines, skip empty trailing/leading segments
+ const rendered: string[] = []
+ for (let i = 0; i < starts.length; i++) {
+ const start = starts[i]!
+ const end = i < starts.length - 1 ? starts[i + 1]! - 1 : content.length
+ const line = content.slice(start, end)
+ if (line) rendered.push(line)
}
- let t3;
- if ($[7] !== t1 || $[8] !== t2) {
- t3 = {t2};
- $[7] = t1;
- $[8] = t2;
- $[9] = t3;
- } else {
- t3 = $[9];
- }
- const t4 = `Showing ${rendered.length} lines`;
- let t5;
- if ($[10] !== bytesTotal || $[11] !== isIncomplete) {
- t5 = isIncomplete ? ` of ${formatFileSize(bytesTotal)}` : "";
- $[10] = bytesTotal;
- $[11] = isIncomplete;
- $[12] = t5;
- } else {
- t5 = $[12];
- }
- let t6;
- if ($[13] !== t4 || $[14] !== t5) {
- t6 = {t4}{t5};
- $[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 {line_0};
+
+ return (
+ <>
+
+ {rendered.map((line, i) => (
+
+ {line}
+
+ ))}
+
+
+ {`Showing ${rendered.length} lines`}
+ {isIncomplete ? ` of ${formatFileSize(bytesTotal)}` : ''}
+
+ >
+ )
}
diff --git a/src/components/tasks/ShellProgress.tsx b/src/components/tasks/ShellProgress.tsx
index 6e9a671c0..b70494c16 100644
--- a/src/components/tasks/ShellProgress.tsx
+++ b/src/components/tasks/ShellProgress.tsx
@@ -1,86 +1,52 @@
-import { c as _c } from "react/compiler-runtime";
-import type { ReactNode } from 'react';
-import React from 'react';
-import { Text } from 'src/ink.js';
-import type { TaskStatus } from 'src/Task.js';
-import type { LocalShellTaskState } from 'src/tasks/LocalShellTask/guards.js';
-import type { DeepImmutable } from 'src/types/utils.js';
+import type { ReactNode } from 'react'
+import React from 'react'
+import { Text } from 'src/ink.js'
+import type { TaskStatus } from 'src/Task.js'
+import type { LocalShellTaskState } from 'src/tasks/LocalShellTask/guards.js'
+import type { DeepImmutable } from 'src/types/utils.js'
+
type TaskStatusTextProps = {
- status: TaskStatus;
- label?: string;
- suffix?: string;
-};
-export function TaskStatusText(t0) {
- const $ = _c(4);
- const {
- status,
- label,
- suffix
- } = t0;
- const displayLabel = label ?? status;
- const color = status === "completed" ? "success" : status === "failed" ? "error" : status === "killed" ? "warning" : undefined;
- let t1;
- if ($[0] !== color || $[1] !== displayLabel || $[2] !== suffix) {
- t1 = ({displayLabel}{suffix});
- $[0] = color;
- $[1] = displayLabel;
- $[2] = suffix;
- $[3] = t1;
- } else {
- t1 = $[3];
- }
- return t1;
+ status: TaskStatus
+ label?: string
+ suffix?: string
}
-export function ShellProgress(t0) {
- const $ = _c(4);
- const {
- shell
- } = t0;
+
+export function TaskStatusText({
+ status,
+ label,
+ suffix,
+}: TaskStatusTextProps): ReactNode {
+ const displayLabel = label ?? status
+ const color =
+ status === 'completed'
+ ? 'success'
+ : status === 'failed'
+ ? 'error'
+ : status === 'killed'
+ ? 'warning'
+ : undefined
+ return (
+
+ ({displayLabel}
+ {suffix})
+
+ )
+}
+
+export function ShellProgress({
+ shell,
+}: {
+ shell: DeepImmutable
+}): ReactNode {
switch (shell.status) {
- case "completed":
- {
- let t1;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = ;
- $[0] = t1;
- } else {
- t1 = $[0];
- }
- return t1;
- }
- case "failed":
- {
- let t1;
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = ;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- return t1;
- }
- case "killed":
- {
- let t1;
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = ;
- $[2] = t1;
- } else {
- t1 = $[2];
- }
- return t1;
- }
- case "running":
- case "pending":
- {
- let t1;
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = ;
- $[3] = t1;
- } else {
- t1 = $[3];
- }
- return t1;
- }
+ case 'completed':
+ return
+ case 'failed':
+ return
+ case 'killed':
+ return
+ case 'running':
+ case 'pending':
+ return
}
}
diff --git a/src/components/tasks/renderToolActivity.tsx b/src/components/tasks/renderToolActivity.tsx
index e2e4ebae7..a6e1c60a2 100644
--- a/src/components/tasks/renderToolActivity.tsx
+++ b/src/components/tasks/renderToolActivity.tsx
@@ -1,32 +1,39 @@
-import React from 'react';
-import { Text } from '../../ink.js';
-import type { Tools } from '../../Tool.js';
-import { findToolByName } from '../../Tool.js';
-import type { ToolActivity } from '../../tasks/LocalAgentTask/LocalAgentTask.js';
-import type { ThemeName } from '../../utils/theme.js';
-export function renderToolActivity(activity: ToolActivity, tools: Tools, theme: ThemeName): React.ReactNode {
- const tool = findToolByName(tools, activity.toolName);
+import React from 'react'
+import { Text } from '../../ink.js'
+import type { Tools } from '../../Tool.js'
+import { findToolByName } from '../../Tool.js'
+import type { ToolActivity } from '../../tasks/LocalAgentTask/LocalAgentTask.js'
+import type { ThemeName } from '../../utils/theme.js'
+
+export function renderToolActivity(
+ activity: ToolActivity,
+ tools: Tools,
+ theme: ThemeName,
+): React.ReactNode {
+ const tool = findToolByName(tools, activity.toolName)
if (!tool) {
- return activity.toolName;
+ return activity.toolName
}
try {
- const parsed = tool.inputSchema.safeParse(activity.input);
- const parsedInput = parsed.success ? parsed.data : {};
- const userFacingName = tool.userFacingName(parsedInput);
+ const parsed = tool.inputSchema.safeParse(activity.input)
+ const parsedInput = parsed.success ? parsed.data : {}
+ const userFacingName = tool.userFacingName(parsedInput)
if (!userFacingName) {
- return activity.toolName;
+ return activity.toolName
}
const toolArgs = tool.renderToolUseMessage(parsedInput, {
theme,
- verbose: false
- });
+ verbose: false,
+ })
if (toolArgs) {
- return
+ return (
+
{userFacingName}({toolArgs})
- ;
+
+ )
}
- return userFacingName;
+ return userFacingName
} catch {
- return activity.toolName;
+ return activity.toolName
}
}
diff --git a/src/components/tasks/taskStatusUtils.tsx b/src/components/tasks/taskStatusUtils.tsx
index a70cbd6ca..91cb14cbf 100644
--- a/src/components/tasks/taskStatusUtils.tsx
+++ b/src/components/tasks/taskStatusUtils.tsx
@@ -2,71 +2,73 @@
* Shared utilities for displaying task status across different task types.
*/
-import figures from 'figures';
-import type { TaskStatus } from 'src/Task.js';
-import type { InProcessTeammateTaskState } from 'src/tasks/InProcessTeammateTask/types.js';
-import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js';
-import { isBackgroundTask, type TaskState } from 'src/tasks/types.js';
-import type { DeepImmutable } from 'src/types/utils.js';
-import { summarizeRecentActivities } from 'src/utils/collapseReadSearch.js';
+import figures from 'figures'
+import type { TaskStatus } from 'src/Task.js'
+import type { InProcessTeammateTaskState } from 'src/tasks/InProcessTeammateTask/types.js'
+import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
+import { isBackgroundTask, type TaskState } from 'src/tasks/types.js'
+import type { DeepImmutable } from 'src/types/utils.js'
+import { summarizeRecentActivities } from 'src/utils/collapseReadSearch.js'
/**
* Returns true if the given task status represents a terminal (finished) state.
*/
export function isTerminalStatus(status: TaskStatus): boolean {
- return status === 'completed' || status === 'failed' || status === 'killed';
+ return status === 'completed' || status === 'failed' || status === 'killed'
}
/**
* Returns the appropriate icon for a task based on status and state flags.
*/
-export function getTaskStatusIcon(status: TaskStatus, options?: {
- isIdle?: boolean;
- awaitingApproval?: boolean;
- hasError?: boolean;
- shutdownRequested?: boolean;
-}): string {
- const {
- isIdle,
- awaitingApproval,
- hasError,
- shutdownRequested
- } = options ?? {};
- if (hasError) return figures.cross;
- if (awaitingApproval) return figures.questionMarkPrefix;
- if (shutdownRequested) return figures.warning;
+export function getTaskStatusIcon(
+ status: TaskStatus,
+ options?: {
+ isIdle?: boolean
+ awaitingApproval?: boolean
+ hasError?: boolean
+ shutdownRequested?: boolean
+ },
+): string {
+ const { isIdle, awaitingApproval, hasError, shutdownRequested } =
+ options ?? {}
+
+ if (hasError) return figures.cross
+ if (awaitingApproval) return figures.questionMarkPrefix
+ if (shutdownRequested) return figures.warning
+
if (status === 'running') {
- if (isIdle) return figures.ellipsis;
- return figures.play;
+ if (isIdle) return figures.ellipsis
+ return figures.play
}
- if (status === 'completed') return figures.tick;
- if (status === 'failed' || status === 'killed') return figures.cross;
- return figures.bullet;
+ if (status === 'completed') return figures.tick
+ if (status === 'failed' || status === 'killed') return figures.cross
+ return figures.bullet
}
/**
* Returns the appropriate semantic color for a task based on status and state flags.
*/
-export function getTaskStatusColor(status: TaskStatus, options?: {
- isIdle?: boolean;
- awaitingApproval?: boolean;
- hasError?: boolean;
- shutdownRequested?: boolean;
-}): 'success' | 'error' | 'warning' | 'background' {
- const {
- isIdle,
- awaitingApproval,
- hasError,
- shutdownRequested
- } = options ?? {};
- if (hasError) return 'error';
- if (awaitingApproval) return 'warning';
- if (shutdownRequested) return 'warning';
- if (isIdle) return 'background';
- if (status === 'completed') return 'success';
- if (status === 'failed') return 'error';
- if (status === 'killed') return 'warning';
- return 'background';
+export function getTaskStatusColor(
+ status: TaskStatus,
+ options?: {
+ isIdle?: boolean
+ awaitingApproval?: boolean
+ hasError?: boolean
+ shutdownRequested?: boolean
+ },
+): 'success' | 'error' | 'warning' | 'background' {
+ const { isIdle, awaitingApproval, hasError, shutdownRequested } =
+ options ?? {}
+
+ if (hasError) return 'error'
+ if (awaitingApproval) return 'warning'
+ if (shutdownRequested) return 'warning'
+ if (isIdle) return 'background'
+
+ if (status === 'completed') return 'success'
+ if (status === 'failed') return 'error'
+ if (status === 'killed') return 'warning'
+ return 'background'
}
/**
@@ -74,11 +76,18 @@ export function getTaskStatusColor(status: TaskStatus, options?: {
* accounting for shutdown/approval/idle states and falling back through
* recent-activity summary → last activity description → 'working'.
*/
-export function describeTeammateActivity(t: DeepImmutable): string {
- if (t.shutdownRequested) return 'stopping';
- if (t.awaitingPlanApproval) return 'awaiting approval';
- if (t.isIdle) return 'idle';
- return (t.progress?.recentActivities && summarizeRecentActivities(t.progress.recentActivities)) ?? t.progress?.lastActivity?.activityDescription ?? 'working';
+export function describeTeammateActivity(
+ t: DeepImmutable,
+): string {
+ if (t.shutdownRequested) return 'stopping'
+ if (t.awaitingPlanApproval) return 'awaiting approval'
+ if (t.isIdle) return 'idle'
+ return (
+ (t.progress?.recentActivities &&
+ summarizeRecentActivities(t.progress.recentActivities)) ??
+ t.progress?.lastActivity?.activityDescription ??
+ 'working'
+ )
}
/**
@@ -90,17 +99,21 @@ export function describeTeammateActivity(t: DeepImmutable{children};
- $[0] = children;
- $[1] = color;
- $[2] = goBack;
- $[3] = subtitle;
- $[4] = t2;
- $[5] = t3;
- } else {
- t3 = $[5];
- }
- let t4;
- if ($[6] !== footerText) {
- t4 = ;
- $[6] = footerText;
- $[7] = t4;
- } else {
- t4 = $[7];
- }
- let t5;
- if ($[8] !== t3 || $[9] !== t4) {
- t5 = <>{t3}{t4}>;
- $[8] = t3;
- $[9] = t4;
- $[10] = t5;
- } else {
- t5 = $[10];
- }
- return t5;
+ goBack,
+ } = useWizard()
+ const title = titleOverride || providerTitle || 'Wizard'
+ const stepSuffix =
+ showStepCounter !== false ? ` (${currentStepIndex + 1}/${totalSteps})` : ''
+
+ return (
+ <>
+
+ {children}
+
+
+ >
+ )
}
diff --git a/src/components/wizard/WizardNavigationFooter.tsx b/src/components/wizard/WizardNavigationFooter.tsx
index 183334a91..35a03ee81 100644
--- a/src/components/wizard/WizardNavigationFooter.tsx
+++ b/src/components/wizard/WizardNavigationFooter.tsx
@@ -1,23 +1,37 @@
-import React, { type ReactNode } from 'react';
-import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
-import { Box, Text } from '../../ink.js';
-import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
-import { Byline } from '../design-system/Byline.js';
-import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
+import React, { type ReactNode } from 'react'
+import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
+import { Box, Text } from '../../ink.js'
+import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
+import { Byline } from '../design-system/Byline.js'
+import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
+
type Props = {
- instructions?: ReactNode;
-};
+ instructions?: ReactNode
+}
+
export function WizardNavigationFooter({
- instructions =
+ instructions = (
+
-
+
+ ),
}: Props): ReactNode {
- const exitState = useExitOnCtrlCDWithKeybindings();
- return
+ const exitState = useExitOnCtrlCDWithKeybindings()
+
+ return (
+
- {exitState.pending ? `Press ${exitState.keyName} again to exit` : instructions}
+ {exitState.pending
+ ? `Press ${exitState.keyName} again to exit`
+ : instructions}
- ;
+
+ )
}
diff --git a/src/components/wizard/WizardProvider.tsx b/src/components/wizard/WizardProvider.tsx
index 3160ea610..6707cb95a 100644
--- a/src/components/wizard/WizardProvider.tsx
+++ b/src/components/wizard/WizardProvider.tsx
@@ -1,156 +1,96 @@
-import { c as _c } from "react/compiler-runtime";
-import React, { createContext, type ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
-import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
-import type { WizardContextValue, WizardProviderProps } from './types.js';
+import React, {
+ createContext,
+ type ReactNode,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react'
+import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
+import type { WizardContextValue, WizardProviderProps } from './types.js'
// Use any here for the context since it will be cast properly when used
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const WizardContext = createContext | null>(null);
-export function WizardProvider(t0) {
- const $ = _c(38);
- const {
- steps,
- initialData: t1,
- onComplete,
- onCancel,
- children,
- title,
- showStepCounter: t2
- } = t0;
- let t3;
- if ($[0] !== t1) {
- t3 = t1 === undefined ? {} as T : t1;
- $[0] = t1;
- $[1] = t3;
- } else {
- t3 = $[1];
- }
- const initialData = t3;
- const showStepCounter = t2 === undefined ? true : t2;
- const [currentStepIndex, setCurrentStepIndex] = useState(0);
- const [wizardData, setWizardData] = useState(initialData);
- const [isCompleted, setIsCompleted] = useState(false);
- let t4;
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
- t4 = [];
- $[2] = t4;
- } else {
- t4 = $[2];
- }
- const [navigationHistory, setNavigationHistory] = useState(t4);
- useExitOnCtrlCDWithKeybindings();
- let t5;
- let t6;
- if ($[3] !== isCompleted || $[4] !== onComplete || $[5] !== wizardData) {
- t5 = () => {
- if (isCompleted) {
- setNavigationHistory([]);
- onComplete(wizardData);
- }
- };
- t6 = [isCompleted, wizardData, onComplete];
- $[3] = isCompleted;
- $[4] = onComplete;
- $[5] = wizardData;
- $[6] = t5;
- $[7] = t6;
- } else {
- t5 = $[6];
- t6 = $[7];
- }
- useEffect(t5, t6);
- let t7;
- if ($[8] !== currentStepIndex || $[9] !== navigationHistory || $[10] !== steps.length) {
- t7 = () => {
- if (currentStepIndex < steps.length - 1) {
- if (navigationHistory.length > 0) {
- setNavigationHistory(prev => [...prev, currentStepIndex]);
- }
- setCurrentStepIndex(_temp);
- } else {
- setIsCompleted(true);
- }
- };
- $[8] = currentStepIndex;
- $[9] = navigationHistory;
- $[10] = steps.length;
- $[11] = t7;
- } else {
- t7 = $[11];
- }
- const goNext = t7;
- let t8;
- if ($[12] !== currentStepIndex || $[13] !== navigationHistory || $[14] !== onCancel) {
- t8 = () => {
+export const WizardContext = createContext | null>(null)
+
+export function WizardProvider>({
+ steps,
+ initialData = {} as T,
+ onComplete,
+ onCancel,
+ children,
+ title,
+ showStepCounter = true,
+}: WizardProviderProps): ReactNode {
+ const [currentStepIndex, setCurrentStepIndex] = useState(0)
+ const [wizardData, setWizardData] = useState(initialData)
+ const [isCompleted, setIsCompleted] = useState(false)
+ const [navigationHistory, setNavigationHistory] = useState([])
+
+ useExitOnCtrlCDWithKeybindings()
+
+ // Handle completion in useEffect to avoid updating parent during render
+ useEffect(() => {
+ if (isCompleted) {
+ setNavigationHistory([])
+ void onComplete(wizardData)
+ }
+ }, [isCompleted, wizardData, onComplete])
+
+ const goNext = useCallback(() => {
+ if (currentStepIndex < steps.length - 1) {
+ // If we have history (non-linear flow), add current step to it
if (navigationHistory.length > 0) {
- const previousStep = navigationHistory[navigationHistory.length - 1];
- if (previousStep !== undefined) {
- setNavigationHistory(_temp2);
- setCurrentStepIndex(previousStep);
- }
- } else {
- if (currentStepIndex > 0) {
- setCurrentStepIndex(_temp3);
- } else {
- if (onCancel) {
- onCancel();
- }
- }
+ setNavigationHistory(prev => [...prev, currentStepIndex])
}
- };
- $[12] = currentStepIndex;
- $[13] = navigationHistory;
- $[14] = onCancel;
- $[15] = t8;
- } else {
- t8 = $[15];
- }
- const goBack = t8;
- let t9;
- if ($[16] !== currentStepIndex || $[17] !== steps.length) {
- t9 = index => {
+
+ setCurrentStepIndex(prev => prev + 1)
+ } else {
+ // Mark as completed, which will trigger useEffect
+ setIsCompleted(true)
+ }
+ }, [currentStepIndex, steps.length, navigationHistory])
+
+ const goBack = useCallback(() => {
+ // Check if we have navigation history to use
+ if (navigationHistory.length > 0) {
+ const previousStep = navigationHistory[navigationHistory.length - 1]
+ if (previousStep !== undefined) {
+ setNavigationHistory(prev => prev.slice(0, -1))
+ setCurrentStepIndex(previousStep)
+ }
+ } else if (currentStepIndex > 0) {
+ // Fallback to simple decrement if no history
+ setCurrentStepIndex(prev => prev - 1)
+ } else if (onCancel) {
+ onCancel()
+ }
+ }, [currentStepIndex, navigationHistory, onCancel])
+
+ const goToStep = useCallback(
+ (index: number) => {
if (index >= 0 && index < steps.length) {
- setNavigationHistory(prev_3 => [...prev_3, currentStepIndex]);
- setCurrentStepIndex(index);
+ // Push current step to history before jumping
+ setNavigationHistory(prev => [...prev, currentStepIndex])
+ setCurrentStepIndex(index)
}
- };
- $[16] = currentStepIndex;
- $[17] = steps.length;
- $[18] = t9;
- } else {
- t9 = $[18];
- }
- const goToStep = t9;
- let t10;
- if ($[19] !== onCancel) {
- t10 = () => {
- setNavigationHistory([]);
- if (onCancel) {
- onCancel();
- }
- };
- $[19] = onCancel;
- $[20] = t10;
- } else {
- t10 = $[20];
- }
- const cancel = t10;
- let t11;
- if ($[21] === Symbol.for("react.memo_cache_sentinel")) {
- t11 = updates => {
- setWizardData(prev_4 => ({
- ...prev_4,
- ...updates
- }));
- };
- $[21] = t11;
- } else {
- t11 = $[21];
- }
- const updateWizardData = t11;
- let t12;
- if ($[22] !== cancel || $[23] !== currentStepIndex || $[24] !== goBack || $[25] !== goNext || $[26] !== goToStep || $[27] !== showStepCounter || $[28] !== steps.length || $[29] !== title || $[30] !== wizardData) {
- t12 = {
+ },
+ [currentStepIndex, steps.length],
+ )
+
+ const cancel = useCallback(() => {
+ setNavigationHistory([])
+ if (onCancel) {
+ onCancel()
+ }
+ }, [onCancel])
+
+ const updateWizardData = useCallback((updates: Partial) => {
+ setWizardData(prev => ({ ...prev, ...updates }))
+ }, [])
+
+ const contextValue = useMemo>(
+ () => ({
currentStepIndex,
totalSteps: steps.length,
wizardData,
@@ -161,52 +101,31 @@ export function WizardProvider(t0) {
goToStep,
cancel,
title,
- showStepCounter
- };
- $[22] = cancel;
- $[23] = currentStepIndex;
- $[24] = goBack;
- $[25] = goNext;
- $[26] = goToStep;
- $[27] = showStepCounter;
- $[28] = steps.length;
- $[29] = title;
- $[30] = wizardData;
- $[31] = t12;
- } else {
- t12 = $[31];
- }
- const contextValue = t12;
- const CurrentStepComponent = steps[currentStepIndex];
+ showStepCounter,
+ }),
+ [
+ currentStepIndex,
+ steps.length,
+ wizardData,
+ updateWizardData,
+ goNext,
+ goBack,
+ goToStep,
+ cancel,
+ title,
+ showStepCounter,
+ ],
+ )
+
+ const CurrentStepComponent = steps[currentStepIndex]
+
if (!CurrentStepComponent || isCompleted) {
- return null;
+ return null
}
- let t13;
- if ($[32] !== CurrentStepComponent || $[33] !== children) {
- t13 = children || ;
- $[32] = CurrentStepComponent;
- $[33] = children;
- $[34] = t13;
- } else {
- t13 = $[34];
- }
- let t14;
- if ($[35] !== contextValue || $[36] !== t13) {
- t14 = {t13};
- $[35] = contextValue;
- $[36] = t13;
- $[37] = t14;
- } else {
- t14 = $[37];
- }
- return t14;
-}
-function _temp3(prev_2) {
- return prev_2 - 1;
-}
-function _temp2(prev_1) {
- return prev_1.slice(0, -1);
-}
-function _temp(prev_0) {
- return prev_0 + 1;
+
+ return (
+
+ {children || }
+
+ )
}