mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-19 23:05:51 +00:00
style: 完成所有文件的lint
This commit is contained in:
@@ -1,147 +1,130 @@
|
||||
// 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
|
||||
: undefined;
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
import { Box, Text, Link } from '@anthropic/ink'
|
||||
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 { Box, Text, Link } from '@anthropic/ink';
|
||||
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 { Byline, KeyboardShortcutHint } from '@anthropic/ink'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { useTasksV2 } from '../../hooks/useTasksV2.js'
|
||||
import { formatDuration, formatFileSize } 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, useHasSelection, useSelection } from '@anthropic/ink'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { getPlatform } from '../../utils/platform.js'
|
||||
import { PrBadge } from '../PrBadge.js'
|
||||
} 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 { Byline, KeyboardShortcutHint } from '@anthropic/ink';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { useTasksV2 } from '../../hooks/useTasksV2.js';
|
||||
import { formatDuration, formatFileSize } 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, useHasSelection, useSelection } from '@anthropic/ink';
|
||||
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;
|
||||
|
||||
const RSS_UPDATE_INTERVAL_MS = 5_000
|
||||
const RSS_UPDATE_INTERVAL_MS = 5_000;
|
||||
|
||||
type RssState = { text: string; level: 'normal' | 'warning' | 'error' }
|
||||
type RssState = { text: string; level: 'normal' | 'warning' | 'error' };
|
||||
|
||||
function useRssDisplay(): RssState | null {
|
||||
const [state, setState] = useState<RssState | null>(null)
|
||||
const [state, setState] = useState<RssState | null>(null);
|
||||
useEffect(() => {
|
||||
function update(): void {
|
||||
const mb = process.memoryUsage().rss / (1024 * 1024)
|
||||
const level = mb >= 1024 ? 'error' : mb >= 512 ? 'warning' : 'normal'
|
||||
const text = formatFileSize(mb * 1024 * 1024)
|
||||
setState(prev => (prev?.text === text ? prev : { text, level }))
|
||||
const mb = process.memoryUsage().rss / (1024 * 1024);
|
||||
const level = mb >= 1024 ? 'error' : mb >= 512 ? 'warning' : 'normal';
|
||||
const text = formatFileSize(mb * 1024 * 1024);
|
||||
setState(prev => (prev?.text === text ? prev : { text, level }));
|
||||
}
|
||||
update()
|
||||
const timer = setInterval(update, RSS_UPDATE_INTERVAL_MS)
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
return state
|
||||
update();
|
||||
const timer = setInterval(update, RSS_UPDATE_INTERVAL_MS);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
return state;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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(): React.ReactNode {
|
||||
const nextTickAt = useSyncExternalStore(
|
||||
proactiveModule?.subscribeToProactiveChanges ?? NO_OP_SUBSCRIBE,
|
||||
proactiveModule?.getNextTickAt ?? NULL,
|
||||
NULL,
|
||||
)
|
||||
);
|
||||
|
||||
const [remainingSeconds, setRemainingSeconds] = useState<number | null>(null)
|
||||
const [remainingSeconds, setRemainingSeconds] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (nextTickAt === null) {
|
||||
setRemainingSeconds(null)
|
||||
return
|
||||
setRemainingSeconds(null);
|
||||
return;
|
||||
}
|
||||
|
||||
function update(): void {
|
||||
const remaining = Math.max(
|
||||
0,
|
||||
Math.ceil((nextTickAt! - Date.now()) / 1000),
|
||||
)
|
||||
setRemainingSeconds(remaining)
|
||||
const remaining = Math.max(0, Math.ceil((nextTickAt! - Date.now()) / 1000));
|
||||
setRemainingSeconds(remaining);
|
||||
}
|
||||
|
||||
update()
|
||||
const interval = setInterval(update, 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [nextTickAt])
|
||||
update();
|
||||
const interval = setInterval(update, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [nextTickAt]);
|
||||
|
||||
if (remainingSeconds === null) return null
|
||||
if (remainingSeconds === null) return null;
|
||||
|
||||
return (
|
||||
<Text dimColor>
|
||||
waiting{' '}
|
||||
{formatDuration(remainingSeconds * 1000, { mostSignificantOnly: true })}
|
||||
</Text>
|
||||
)
|
||||
return <Text dimColor>waiting {formatDuration(remainingSeconds * 1000, { mostSignificantOnly: true })}</Text>;
|
||||
}
|
||||
|
||||
export function PromptInputFooterLeftSide({
|
||||
@@ -167,26 +150,22 @@ export function PromptInputFooterLeftSide({
|
||||
<Text dimColor key="exit-message">
|
||||
Press {exitMessage.key} again to exit
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
}
|
||||
if (isPasting) {
|
||||
return (
|
||||
<Text dimColor key="pasting-message">
|
||||
Pasting text…
|
||||
</Text>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const showVim = isVimModeEnabled() && vimMode === 'INSERT' && !isSearching
|
||||
const showVim = isVimModeEnabled() && vimMode === 'INSERT' && !isSearching;
|
||||
|
||||
return (
|
||||
<Box justifyContent="flex-start" gap={1}>
|
||||
{isSearching && (
|
||||
<HistorySearchInput
|
||||
value={historyQuery}
|
||||
onChange={setHistoryQuery}
|
||||
historyFailedMatch={historyFailedMatch}
|
||||
/>
|
||||
<HistorySearchInput value={historyQuery} onChange={setHistoryQuery} historyFailedMatch={historyFailedMatch} />
|
||||
)}
|
||||
{showVim ? (
|
||||
<Text dimColor key="vim-insert">
|
||||
@@ -205,20 +184,20 @@ export function PromptInputFooterLeftSide({
|
||||
onOpenTasksDialog={onOpenTasksDialog}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -231,109 +210,70 @@ function ModeIndicator({
|
||||
teammateFooterIndex,
|
||||
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 => s.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 => 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 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,
|
||||
)
|
||||
const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false
|
||||
const voiceState = feature('VOICE_MODE')
|
||||
?
|
||||
useVoiceState(s => s.voiceState)
|
||||
: ('idle' as const)
|
||||
const voiceWarmingUp = feature('VOICE_MODE')
|
||||
?
|
||||
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 voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false;
|
||||
const voiceState = feature('VOICE_MODE') ? useVoiceState(s => s.voiceState) : ('idle' as const);
|
||||
const voiceWarmingUp = feature('VOICE_MODE') ? 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)),
|
||||
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')
|
||||
?
|
||||
useShortcutDisplay('voice:pushToTalk', 'Chat', 'Space')
|
||||
: ''
|
||||
);
|
||||
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') ? 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')
|
||||
?
|
||||
useState(
|
||||
() =>
|
||||
(getGlobalConfig().voiceFooterHintSeenCount ?? 0) <
|
||||
MAX_VOICE_HINT_SHOWS,
|
||||
)
|
||||
: [false]
|
||||
const voiceHintIncrementedRef = feature('VOICE_MODE') ? useRef(false) : null
|
||||
? useState(() => (getGlobalConfig().voiceFooterHintSeenCount ?? 0) < MAX_VOICE_HINT_SHOWS)
|
||||
: [false];
|
||||
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 => s.notifications.current?.key === 'kill-agents-confirm',
|
||||
)
|
||||
const rssState = useRssDisplay()
|
||||
}, [voiceEnabled, voiceHintUnderCap]);
|
||||
const isKillAgentsConfirmShowing = useAppState(s => s.notifications.current?.key === 'kill-agents-confirm');
|
||||
const rssState = useRssDisplay();
|
||||
|
||||
// Derive team info from teamContext (no filesystem I/O needed)
|
||||
// Match the same logic as TeamStatus to avoid trailing separator
|
||||
@@ -342,27 +282,21 @@ function ModeIndicator({
|
||||
isAgentSwarmsEnabled() &&
|
||||
!isInProcessEnabled() &&
|
||||
teamContext !== undefined &&
|
||||
count(Object.values(teamContext.teammates), t => t.name !== 'team-lead') > 0
|
||||
count(Object.values(teamContext.teammates), t => t.name !== 'team-lead') > 0;
|
||||
|
||||
if (mode === 'bash') {
|
||||
return <Text color="bashBorder">! for bash mode</Text>
|
||||
return <Text color="bashBorder">! for bash mode</Text>;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -374,19 +308,16 @@ function ModeIndicator({
|
||||
prStatus.reviewState !== null &&
|
||||
prStatus.url !== null &&
|
||||
primaryItemCount < 2 &&
|
||||
(primaryItemCount === 0 || columns >= 80)
|
||||
(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 => t.type === 'in_process_teammate')
|
||||
const hasTeammatePills =
|
||||
hasInProcessTeammates || (!showSpinnerTree && isViewingTeammate)
|
||||
!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.
|
||||
@@ -395,20 +326,15 @@ function ModeIndicator({
|
||||
const modePart =
|
||||
currentMode && hasActiveMode && !getIsRemoteMode() ? (
|
||||
<Text color={getModeColor(currentMode)} key="mode">
|
||||
{permissionModeSymbol(currentMode)}{' '}
|
||||
{permissionModeTitle(currentMode).toLowerCase()} on
|
||||
{permissionModeSymbol(currentMode)} {permissionModeTitle(currentMode).toLowerCase()} on
|
||||
{shouldShowModeHint && (
|
||||
<Text dimColor>
|
||||
{' '}
|
||||
<KeyboardShortcutHint
|
||||
shortcut={modeCycleShortcut}
|
||||
action="cycle"
|
||||
parens
|
||||
/>
|
||||
<KeyboardShortcutHint shortcut={modeCycleShortcut} action="cycle" parens />
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
) : null
|
||||
) : null;
|
||||
|
||||
// Build parts array - exclude BackgroundTaskStatus when we have teammate pills
|
||||
// (teammate pills get their own row)
|
||||
@@ -425,27 +351,12 @@ function ModeIndicator({
|
||||
// its click-target Box isn't nested inside the <Text wrap="truncate">
|
||||
// wrapper (reconciler throws on Box-in-Text).
|
||||
// Tmux pill (ant-only) — appears right after tasks in nav order
|
||||
...(process.env.USER_TYPE === 'ant' && hasTmuxSession
|
||||
? [<TungstenPill key="tmux" selected={tmuxSelected} />]
|
||||
: []),
|
||||
...(process.env.USER_TYPE === 'ant' && hasTmuxSession ? [<TungstenPill key="tmux" selected={tmuxSelected} />] : []),
|
||||
...(isAgentSwarmsEnabled() && hasTeams
|
||||
? [
|
||||
<TeamStatus
|
||||
key="teams"
|
||||
teamsSelected={teamsSelected}
|
||||
showHint={showHint && !hasBackgroundTasks}
|
||||
/>,
|
||||
]
|
||||
? [<TeamStatus key="teams" teamsSelected={teamsSelected} showHint={showHint && !hasBackgroundTasks} />]
|
||||
: []),
|
||||
...(shouldShowPrStatus
|
||||
? [
|
||||
<PrBadge
|
||||
key="pr-status"
|
||||
number={prStatus.number!}
|
||||
url={prStatus.url!}
|
||||
reviewState={prStatus.reviewState!}
|
||||
/>,
|
||||
]
|
||||
? [<PrBadge key="pr-status" number={prStatus.number!} url={prStatus.url!} reviewState={prStatus.reviewState!} />]
|
||||
: []),
|
||||
// RSS memory indicator — always visible
|
||||
...(rssState
|
||||
@@ -459,15 +370,13 @@ function ModeIndicator({
|
||||
</Text>,
|
||||
]
|
||||
: []),
|
||||
]
|
||||
];
|
||||
|
||||
// Check if any in-process teammates exist (for hint text cycling)
|
||||
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',
|
||||
)
|
||||
);
|
||||
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
|
||||
@@ -482,32 +391,25 @@ function ModeIndicator({
|
||||
hasRunningAgentTasks,
|
||||
isKillAgentsConfirmShowing,
|
||||
)
|
||||
: []
|
||||
: [];
|
||||
|
||||
if (isViewingCompletedTeammate) {
|
||||
parts.push(
|
||||
<Text dimColor key="esc-return">
|
||||
<KeyboardShortcutHint
|
||||
shortcut={escShortcut}
|
||||
action="return to team lead"
|
||||
/>
|
||||
<KeyboardShortcutHint shortcut={escShortcut} action="return to team lead" />
|
||||
</Text>,
|
||||
)
|
||||
);
|
||||
} else if ((feature('PROACTIVE') || feature('KAIROS')) && hasNextTick) {
|
||||
parts.push(<ProactiveCountdown key="proactive" />)
|
||||
parts.push(<ProactiveCountdown key="proactive" />);
|
||||
} 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),
|
||||
]
|
||||
const otherParts = [...(modePart ? [modePart] : []), ...parts, ...(isViewingCompletedTeammate ? [] : hintParts)];
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
@@ -525,21 +427,18 @@ function ModeIndicator({
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 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 <Text wrap="truncate"> — 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) ? (
|
||||
hasBackgroundTasks && !hasTeammatePills && !shouldHideTasksFooter(tasks, showSpinnerTree) ? (
|
||||
<BackgroundTaskStatus
|
||||
tasksSelected={tasksSelected}
|
||||
isViewingTeammate={isViewingTeammate}
|
||||
@@ -547,27 +446,27 @@ function ModeIndicator({
|
||||
isLeaderIdle={!isLoading}
|
||||
onOpenDialog={onOpenTasksDialog}
|
||||
/>
|
||||
) : null
|
||||
) : null;
|
||||
|
||||
if (parts.length === 0 && !tasksPart && !modePart && showHint) {
|
||||
parts.push(
|
||||
<Text dimColor key="shortcuts-hint">
|
||||
? for shortcuts
|
||||
</Text>,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 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(<VoiceWarmupHint key="voice-warmup" />)
|
||||
parts.push(<VoiceWarmupHint key="voice-warmup" />);
|
||||
} else if (isFullscreenEnvEnabled() && selectionHintHasContent) {
|
||||
// xterm.js (VS Code/Cursor/Windsurf) force-selection modifier is
|
||||
// platform-specific and gated on macOS (SelectionService.shouldForceSelection):
|
||||
@@ -579,26 +478,21 @@ 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)
|
||||
const isMac = getPlatform() === 'macos';
|
||||
const altClickFailed = isMac && (selGetState()?.lastPressHadAlt ?? false);
|
||||
parts.push(
|
||||
<Text dimColor key="selection-copy">
|
||||
<Byline>
|
||||
{!copyOnSelect && (
|
||||
<KeyboardShortcutHint shortcut="ctrl+c" action="copy" />
|
||||
)}
|
||||
{!copyOnSelect && <KeyboardShortcutHint shortcut="ctrl+c" action="copy" />}
|
||||
{isXtermJs() &&
|
||||
(altClickFailed ? (
|
||||
<Text>set macOptionClickForcesSelection in VS Code settings</Text>
|
||||
) : (
|
||||
<KeyboardShortcutHint
|
||||
shortcut={isMac ? 'option+click' : 'shift+click'}
|
||||
action="native select"
|
||||
/>
|
||||
<KeyboardShortcutHint shortcut={isMac ? 'option+click' : 'shift+click'} action="native select" />
|
||||
))}
|
||||
</Byline>
|
||||
</Text>,
|
||||
)
|
||||
);
|
||||
} else if (
|
||||
feature('VOICE_MODE') &&
|
||||
parts.length > 0 &&
|
||||
@@ -612,7 +506,7 @@ function ModeIndicator({
|
||||
<Text dimColor key="voice-hint">
|
||||
hold {voiceKeyShortcut} to speak
|
||||
</Text>,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ((tasksPart || hasCoordinatorTasks) && showHint && !hasTeams) {
|
||||
@@ -624,7 +518,7 @@ function ModeIndicator({
|
||||
<KeyboardShortcutHint shortcut="↓" action="manage" />
|
||||
)}
|
||||
</Text>,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// In fullscreen the bottom section is flexShrink:0 — every row here
|
||||
@@ -636,7 +530,7 @@ 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() ? <Text> </Text> : null
|
||||
return isFullscreenEnvEnabled() ? <Text> </Text> : null;
|
||||
}
|
||||
|
||||
// flexShrink=0 keeps mode + pill at natural width; the remaining parts
|
||||
@@ -661,7 +555,7 @@ function ModeIndicator({
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function getSpinnerHintParts(
|
||||
@@ -675,27 +569,27 @@ function getSpinnerHintParts(
|
||||
hasRunningAgentTasks: boolean,
|
||||
isKillAgentsConfirmShowing: boolean,
|
||||
): React.ReactElement[] {
|
||||
let toggleAction: string
|
||||
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
|
||||
const showToggleHint = hasTaskItems || hasTeammates;
|
||||
|
||||
return [
|
||||
...(isLoading
|
||||
@@ -708,26 +602,20 @@ function getSpinnerHintParts(
|
||||
...(!isLoading && hasRunningAgentTasks && !isKillAgentsConfirmShowing
|
||||
? [
|
||||
<Text dimColor key="kill-agents">
|
||||
<KeyboardShortcutHint
|
||||
shortcut={killAgentsShortcut}
|
||||
action="stop agents"
|
||||
/>
|
||||
<KeyboardShortcutHint shortcut={killAgentsShortcut} action="stop agents" />
|
||||
</Text>,
|
||||
]
|
||||
: []),
|
||||
...(showToggleHint
|
||||
? [
|
||||
<Text dimColor key="toggle-tasks">
|
||||
<KeyboardShortcutHint
|
||||
shortcut={todosShortcut}
|
||||
action={toggleAction}
|
||||
/>
|
||||
<KeyboardShortcutHint shortcut={todosShortcut} action={toggleAction} />
|
||||
</Text>,
|
||||
]
|
||||
: []),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
function isPrStatusEnabled(): boolean {
|
||||
return getGlobalConfig().prStatusFooterEnabled ?? true
|
||||
return getGlobalConfig().prStatusFooterEnabled ?? true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user