mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 06:15:51 +00:00
更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)
* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-2): 格式化 commands (79 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-5): 格式化 components其余 + hooks + tools (232 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md - README.md: 大幅重写,更详细版本历史和配置示例 - Run.ps1: 新增 Windows 启动脚本 - TODO.md: 新增包完成清单 - V6.md: 删除(架构重构规划已不适用) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复以前的问题 * fix: 修复 login 面板的问题 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,50 +1,38 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import TextInput from '../TextInput.js';
|
||||
import * as React from 'react'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import TextInput from '../TextInput.js'
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
historyFailedMatch: boolean;
|
||||
};
|
||||
function HistorySearchInput(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
historyFailedMatch
|
||||
} = t0;
|
||||
const t1 = historyFailedMatch ? "no matching prompt:" : "search prompts:";
|
||||
let t2;
|
||||
if ($[0] !== t1) {
|
||||
t2 = <Text dimColor={true}>{t1}</Text>;
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const t3 = stringWidth(value) + 1;
|
||||
let t4;
|
||||
if ($[2] !== onChange || $[3] !== t3 || $[4] !== value) {
|
||||
t4 = <TextInput value={value} onChange={onChange} cursorOffset={value.length} onChangeCursorOffset={_temp} columns={t3} focus={true} showCursor={true} multiline={false} dimColor={true} />;
|
||||
$[2] = onChange;
|
||||
$[3] = t3;
|
||||
$[4] = value;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
let t5;
|
||||
if ($[6] !== t2 || $[7] !== t4) {
|
||||
t5 = <Box gap={1}>{t2}{t4}</Box>;
|
||||
$[6] = t2;
|
||||
$[7] = t4;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
return t5;
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
historyFailedMatch: boolean
|
||||
}
|
||||
function _temp() {}
|
||||
export default HistorySearchInput;
|
||||
|
||||
function HistorySearchInput({
|
||||
value,
|
||||
onChange,
|
||||
historyFailedMatch,
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<Box gap={1}>
|
||||
<Text dimColor>
|
||||
{historyFailedMatch ? 'no matching prompt:' : 'search prompts:'}
|
||||
</Text>
|
||||
<TextInput
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
// Force cursor to end of search input since navigation should cancel search
|
||||
cursorOffset={value.length}
|
||||
onChangeCursorOffset={() => {}}
|
||||
columns={stringWidth(value) + 1}
|
||||
focus={true}
|
||||
showCursor={true}
|
||||
multiline={false}
|
||||
dimColor={true}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default HistorySearchInput
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
import * as React from 'react';
|
||||
import { FLAG_ICON } from '../../constants/figures.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import * as React from 'react'
|
||||
import { FLAG_ICON } from '../../constants/figures.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
/**
|
||||
* ANT-ONLY: Banner shown in the transcript that prompts users to report
|
||||
* issues via /issue. Appears when friction is detected in the conversation.
|
||||
*/
|
||||
export function IssueFlagBanner() {
|
||||
return null;
|
||||
export function IssueFlagBanner(): React.ReactNode {
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" marginTop={1} width="100%">
|
||||
<Box minWidth={2}>
|
||||
<Text color="warning">{FLAG_ICON}</Text>
|
||||
</Box>
|
||||
<Text>
|
||||
<Text dimColor>[ANT-ONLY] </Text>
|
||||
<Text color="warning" bold>
|
||||
Something off with Claude?
|
||||
</Text>
|
||||
<Text dimColor> /issue to report it</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,218 +1,201 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
import { type Notification, useNotifications } from 'src/context/notifications.js';
|
||||
import { logEvent } from 'src/services/analytics/index.js';
|
||||
import { useAppState } from 'src/state/AppState.js';
|
||||
import { useVoiceState } from '../../context/voice.js';
|
||||
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js';
|
||||
import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js';
|
||||
import type { IDESelection } from '../../hooks/useIdeSelection.js';
|
||||
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
|
||||
import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js';
|
||||
import { calculateTokenWarningState } from '../../services/compact/autoCompact.js';
|
||||
import type { MCPServerConnection } from '../../services/mcp/types.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import { getApiKeyHelperElapsedMs, getConfiguredApiKeyHelper, getSubscriptionType } from '../../utils/auth.js';
|
||||
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js';
|
||||
import { getExternalEditor } from '../../utils/editor.js';
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
||||
import { formatDuration } from '../../utils/format.js';
|
||||
import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js';
|
||||
import { toIDEDisplayName } from '../../utils/ide.js';
|
||||
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js';
|
||||
import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js';
|
||||
import { AutoUpdaterWrapper } from '../AutoUpdaterWrapper.js';
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
||||
import { IdeStatusIndicator } from '../IdeStatusIndicator.js';
|
||||
import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js';
|
||||
import { SentryErrorBoundary } from '../SentryErrorBoundary.js';
|
||||
import { TokenWarning } from '../TokenWarning.js';
|
||||
import { SandboxPromptFooterHint } from './SandboxPromptFooterHint.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import { type ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
type Notification,
|
||||
useNotifications,
|
||||
} from 'src/context/notifications.js'
|
||||
import { logEvent } from 'src/services/analytics/index.js'
|
||||
import { useAppState } from 'src/state/AppState.js'
|
||||
import { useVoiceState } from '../../context/voice.js'
|
||||
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
|
||||
import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js'
|
||||
import type { IDESelection } from '../../hooks/useIdeSelection.js'
|
||||
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
|
||||
import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'
|
||||
import { calculateTokenWarningState } from '../../services/compact/autoCompact.js'
|
||||
import type { MCPServerConnection } from '../../services/mcp/types.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import {
|
||||
getApiKeyHelperElapsedMs,
|
||||
getConfiguredApiKeyHelper,
|
||||
getSubscriptionType,
|
||||
} from '../../utils/auth.js'
|
||||
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
|
||||
import { getExternalEditor } from '../../utils/editor.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { formatDuration } from '../../utils/format.js'
|
||||
import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js'
|
||||
import { toIDEDisplayName } from '../../utils/ide.js'
|
||||
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'
|
||||
import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js'
|
||||
import { AutoUpdaterWrapper } from '../AutoUpdaterWrapper.js'
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
|
||||
import { IdeStatusIndicator } from '../IdeStatusIndicator.js'
|
||||
import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js'
|
||||
import { SentryErrorBoundary } from '../SentryErrorBoundary.js'
|
||||
import { TokenWarning } from '../TokenWarning.js'
|
||||
import { SandboxPromptFooterHint } from './SandboxPromptFooterHint.js'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const VoiceIndicator: typeof import('./VoiceIndicator.js').VoiceIndicator = feature('VOICE_MODE') ? require('./VoiceIndicator.js').VoiceIndicator : () => null;
|
||||
const VoiceIndicator: typeof import('./VoiceIndicator.js').VoiceIndicator =
|
||||
feature('VOICE_MODE')
|
||||
? require('./VoiceIndicator.js').VoiceIndicator
|
||||
: () => null
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
export const FOOTER_TEMPORARY_STATUS_TIMEOUT = 5000;
|
||||
export const FOOTER_TEMPORARY_STATUS_TIMEOUT = 5000
|
||||
|
||||
type Props = {
|
||||
apiKeyStatus: VerificationStatus;
|
||||
autoUpdaterResult: AutoUpdaterResult | null;
|
||||
isAutoUpdating: boolean;
|
||||
debug: boolean;
|
||||
verbose: boolean;
|
||||
messages: Message[];
|
||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void;
|
||||
ideSelection: IDESelection | undefined;
|
||||
mcpClients?: MCPServerConnection[];
|
||||
isInputWrapped?: boolean;
|
||||
isNarrow?: boolean;
|
||||
};
|
||||
export function Notifications(t0) {
|
||||
const $ = _c(34);
|
||||
const {
|
||||
apiKeyStatus,
|
||||
autoUpdaterResult,
|
||||
debug,
|
||||
isAutoUpdating,
|
||||
verbose,
|
||||
messages,
|
||||
onAutoUpdaterResult,
|
||||
onChangeIsUpdating,
|
||||
ideSelection,
|
||||
mcpClients,
|
||||
isInputWrapped: t1,
|
||||
isNarrow: t2
|
||||
} = t0;
|
||||
const isInputWrapped = t1 === undefined ? false : t1;
|
||||
const isNarrow = t2 === undefined ? false : t2;
|
||||
let t3;
|
||||
if ($[0] !== messages) {
|
||||
const messagesForTokenCount = getMessagesAfterCompactBoundary(messages);
|
||||
t3 = tokenCountFromLastAPIResponse(messagesForTokenCount);
|
||||
$[0] = messages;
|
||||
$[1] = t3;
|
||||
} else {
|
||||
t3 = $[1];
|
||||
}
|
||||
const tokenUsage = t3;
|
||||
const mainLoopModel = useMainLoopModel();
|
||||
let t4;
|
||||
if ($[2] !== mainLoopModel || $[3] !== tokenUsage) {
|
||||
t4 = calculateTokenWarningState(tokenUsage, mainLoopModel);
|
||||
$[2] = mainLoopModel;
|
||||
$[3] = tokenUsage;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
const isShowingCompactMessage = t4.isAboveWarningThreshold;
|
||||
const {
|
||||
status: ideStatus
|
||||
} = useIdeConnectionStatus(mcpClients);
|
||||
const notifications = useAppState(_temp);
|
||||
const {
|
||||
apiKeyStatus: VerificationStatus
|
||||
autoUpdaterResult: AutoUpdaterResult | null
|
||||
isAutoUpdating: boolean
|
||||
debug: boolean
|
||||
verbose: boolean
|
||||
messages: Message[]
|
||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void
|
||||
ideSelection: IDESelection | undefined
|
||||
mcpClients?: MCPServerConnection[]
|
||||
isInputWrapped?: boolean
|
||||
isNarrow?: boolean
|
||||
}
|
||||
|
||||
export function Notifications({
|
||||
apiKeyStatus,
|
||||
autoUpdaterResult,
|
||||
debug,
|
||||
isAutoUpdating,
|
||||
verbose,
|
||||
messages,
|
||||
onAutoUpdaterResult,
|
||||
onChangeIsUpdating,
|
||||
ideSelection,
|
||||
mcpClients,
|
||||
isInputWrapped = false,
|
||||
isNarrow = false,
|
||||
}: Props): ReactNode {
|
||||
const tokenUsage = useMemo(() => {
|
||||
const messagesForTokenCount = getMessagesAfterCompactBoundary(messages)
|
||||
return tokenCountFromLastAPIResponse(messagesForTokenCount)
|
||||
}, [messages])
|
||||
|
||||
// AppState-sourced model — same source as API requests. getMainLoopModel()
|
||||
// re-reads settings.json on every call, so another session's /model write
|
||||
// would leak into this session's display (anthropics/claude-code#37596).
|
||||
const mainLoopModel = useMainLoopModel()
|
||||
const isShowingCompactMessage = calculateTokenWarningState(
|
||||
tokenUsage,
|
||||
mainLoopModel,
|
||||
).isAboveWarningThreshold
|
||||
const { status: ideStatus } = useIdeConnectionStatus(mcpClients)
|
||||
const notifications = useAppState(s => s.notifications)
|
||||
const { addNotification, removeNotification } = useNotifications()
|
||||
const claudeAiLimits = useClaudeAiLimits()
|
||||
|
||||
// Register env hook notifier for CwdChanged/FileChanged feedback
|
||||
useEffect(() => {
|
||||
setEnvHookNotifier((text, isError) => {
|
||||
addNotification({
|
||||
key: 'env-hook',
|
||||
text,
|
||||
color: isError ? 'error' : undefined,
|
||||
priority: isError ? 'medium' : 'low',
|
||||
timeoutMs: isError ? 8000 : 5000,
|
||||
})
|
||||
})
|
||||
return () => setEnvHookNotifier(null)
|
||||
}, [addNotification])
|
||||
|
||||
// Check if we should show the IDE selection indicator
|
||||
const shouldShowIdeSelection =
|
||||
ideStatus === 'connected' &&
|
||||
(ideSelection?.filePath ||
|
||||
(ideSelection?.text && ideSelection.lineCount > 0))
|
||||
|
||||
// Hide update installed message when showing IDE selection
|
||||
const shouldShowAutoUpdater =
|
||||
!shouldShowIdeSelection ||
|
||||
isAutoUpdating ||
|
||||
autoUpdaterResult?.status !== 'success'
|
||||
|
||||
// Check if we're in overage mode for UI indicators
|
||||
const isInOverageMode = claudeAiLimits.isUsingOverage
|
||||
const subscriptionType = getSubscriptionType()
|
||||
const isTeamOrEnterprise =
|
||||
subscriptionType === 'team' || subscriptionType === 'enterprise'
|
||||
|
||||
// Check if the external editor hint should be shown
|
||||
const editor = getExternalEditor()
|
||||
const shouldShowExternalEditorHint =
|
||||
isInputWrapped &&
|
||||
!isShowingCompactMessage &&
|
||||
apiKeyStatus !== 'invalid' &&
|
||||
apiKeyStatus !== 'missing' &&
|
||||
editor !== undefined
|
||||
|
||||
// Show external editor hint as notification when input is wrapped
|
||||
useEffect(() => {
|
||||
if (shouldShowExternalEditorHint && editor) {
|
||||
logEvent('tengu_external_editor_hint_shown', {})
|
||||
addNotification({
|
||||
key: 'external-editor-hint',
|
||||
jsx: (
|
||||
<Text dimColor>
|
||||
<ConfigurableShortcutHint
|
||||
action="chat:externalEditor"
|
||||
context="Chat"
|
||||
fallback="ctrl+g"
|
||||
description={`edit in ${toIDEDisplayName(editor)}`}
|
||||
/>
|
||||
</Text>
|
||||
),
|
||||
priority: 'immediate',
|
||||
timeoutMs: 5000,
|
||||
})
|
||||
} else {
|
||||
removeNotification('external-editor-hint')
|
||||
}
|
||||
}, [
|
||||
shouldShowExternalEditorHint,
|
||||
editor,
|
||||
addNotification,
|
||||
removeNotification
|
||||
} = useNotifications();
|
||||
const claudeAiLimits = useClaudeAiLimits();
|
||||
let t5;
|
||||
let t6;
|
||||
if ($[5] !== addNotification) {
|
||||
t5 = () => {
|
||||
setEnvHookNotifier((text, isError) => {
|
||||
addNotification({
|
||||
key: "env-hook",
|
||||
text,
|
||||
color: isError ? "error" : undefined,
|
||||
priority: isError ? "medium" : "low",
|
||||
timeoutMs: isError ? 8000 : 5000
|
||||
});
|
||||
});
|
||||
return _temp2;
|
||||
};
|
||||
t6 = [addNotification];
|
||||
$[5] = addNotification;
|
||||
$[6] = t5;
|
||||
$[7] = t6;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
t6 = $[7];
|
||||
}
|
||||
useEffect(t5, t6);
|
||||
const shouldShowIdeSelection = ideStatus === "connected" && (ideSelection?.filePath || ideSelection?.text && ideSelection.lineCount > 0);
|
||||
const shouldShowAutoUpdater = !shouldShowIdeSelection || isAutoUpdating || autoUpdaterResult?.status !== "success";
|
||||
const isInOverageMode = claudeAiLimits.isUsingOverage;
|
||||
let t7;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = getSubscriptionType();
|
||||
$[8] = t7;
|
||||
} else {
|
||||
t7 = $[8];
|
||||
}
|
||||
const subscriptionType = t7;
|
||||
const isTeamOrEnterprise = subscriptionType === "team" || subscriptionType === "enterprise";
|
||||
let t8;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = getExternalEditor();
|
||||
$[9] = t8;
|
||||
} else {
|
||||
t8 = $[9];
|
||||
}
|
||||
const editor = t8;
|
||||
const shouldShowExternalEditorHint = isInputWrapped && !isShowingCompactMessage && apiKeyStatus !== "invalid" && apiKeyStatus !== "missing" && editor !== undefined;
|
||||
let t10;
|
||||
let t9;
|
||||
if ($[10] !== addNotification || $[11] !== removeNotification || $[12] !== shouldShowExternalEditorHint) {
|
||||
t9 = () => {
|
||||
if (shouldShowExternalEditorHint && editor) {
|
||||
logEvent("tengu_external_editor_hint_shown", {});
|
||||
addNotification({
|
||||
key: "external-editor-hint",
|
||||
jsx: <Text dimColor={true}><ConfigurableShortcutHint action="chat:externalEditor" context="Chat" fallback="ctrl+g" description={`edit in ${toIDEDisplayName(editor)}`} /></Text>,
|
||||
priority: "immediate",
|
||||
timeoutMs: 5000
|
||||
});
|
||||
} else {
|
||||
removeNotification("external-editor-hint");
|
||||
}
|
||||
};
|
||||
t10 = [shouldShowExternalEditorHint, editor, addNotification, removeNotification];
|
||||
$[10] = addNotification;
|
||||
$[11] = removeNotification;
|
||||
$[12] = shouldShowExternalEditorHint;
|
||||
$[13] = t10;
|
||||
$[14] = t9;
|
||||
} else {
|
||||
t10 = $[13];
|
||||
t9 = $[14];
|
||||
}
|
||||
useEffect(t9, t10);
|
||||
const t11 = isNarrow ? "flex-start" : "flex-end";
|
||||
const t12 = isInOverageMode ?? false;
|
||||
let t13;
|
||||
if ($[15] !== apiKeyStatus || $[16] !== autoUpdaterResult || $[17] !== debug || $[18] !== ideSelection || $[19] !== isAutoUpdating || $[20] !== isShowingCompactMessage || $[21] !== mainLoopModel || $[22] !== mcpClients || $[23] !== notifications || $[24] !== onAutoUpdaterResult || $[25] !== onChangeIsUpdating || $[26] !== shouldShowAutoUpdater || $[27] !== t12 || $[28] !== tokenUsage || $[29] !== verbose) {
|
||||
t13 = <NotificationContent ideSelection={ideSelection} mcpClients={mcpClients} notifications={notifications} isInOverageMode={t12} isTeamOrEnterprise={isTeamOrEnterprise} apiKeyStatus={apiKeyStatus} debug={debug} verbose={verbose} tokenUsage={tokenUsage} mainLoopModel={mainLoopModel} shouldShowAutoUpdater={shouldShowAutoUpdater} autoUpdaterResult={autoUpdaterResult} isAutoUpdating={isAutoUpdating} isShowingCompactMessage={isShowingCompactMessage} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={onChangeIsUpdating} />;
|
||||
$[15] = apiKeyStatus;
|
||||
$[16] = autoUpdaterResult;
|
||||
$[17] = debug;
|
||||
$[18] = ideSelection;
|
||||
$[19] = isAutoUpdating;
|
||||
$[20] = isShowingCompactMessage;
|
||||
$[21] = mainLoopModel;
|
||||
$[22] = mcpClients;
|
||||
$[23] = notifications;
|
||||
$[24] = onAutoUpdaterResult;
|
||||
$[25] = onChangeIsUpdating;
|
||||
$[26] = shouldShowAutoUpdater;
|
||||
$[27] = t12;
|
||||
$[28] = tokenUsage;
|
||||
$[29] = verbose;
|
||||
$[30] = t13;
|
||||
} else {
|
||||
t13 = $[30];
|
||||
}
|
||||
let t14;
|
||||
if ($[31] !== t11 || $[32] !== t13) {
|
||||
t14 = <SentryErrorBoundary><Box flexDirection="column" alignItems={t11} flexShrink={0} overflowX="hidden">{t13}</Box></SentryErrorBoundary>;
|
||||
$[31] = t11;
|
||||
$[32] = t13;
|
||||
$[33] = t14;
|
||||
} else {
|
||||
t14 = $[33];
|
||||
}
|
||||
return t14;
|
||||
}
|
||||
function _temp2() {
|
||||
return setEnvHookNotifier(null);
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.notifications;
|
||||
removeNotification,
|
||||
])
|
||||
|
||||
return (
|
||||
<SentryErrorBoundary>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
||||
flexShrink={0}
|
||||
overflowX="hidden"
|
||||
>
|
||||
<NotificationContent
|
||||
ideSelection={ideSelection}
|
||||
mcpClients={mcpClients}
|
||||
notifications={notifications}
|
||||
isInOverageMode={isInOverageMode ?? false}
|
||||
isTeamOrEnterprise={isTeamOrEnterprise}
|
||||
apiKeyStatus={apiKeyStatus}
|
||||
debug={debug}
|
||||
verbose={verbose}
|
||||
tokenUsage={tokenUsage}
|
||||
mainLoopModel={mainLoopModel}
|
||||
shouldShowAutoUpdater={shouldShowAutoUpdater}
|
||||
autoUpdaterResult={autoUpdaterResult}
|
||||
isAutoUpdating={isAutoUpdating}
|
||||
isShowingCompactMessage={isShowingCompactMessage}
|
||||
onAutoUpdaterResult={onAutoUpdaterResult}
|
||||
onChangeIsUpdating={onChangeIsUpdating}
|
||||
/>
|
||||
</Box>
|
||||
</SentryErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
function NotificationContent({
|
||||
ideSelection,
|
||||
mcpClients,
|
||||
@@ -229,103 +212,155 @@ function NotificationContent({
|
||||
isAutoUpdating,
|
||||
isShowingCompactMessage,
|
||||
onAutoUpdaterResult,
|
||||
onChangeIsUpdating
|
||||
onChangeIsUpdating,
|
||||
}: {
|
||||
ideSelection: IDESelection | undefined;
|
||||
mcpClients?: MCPServerConnection[];
|
||||
ideSelection: IDESelection | undefined
|
||||
mcpClients?: MCPServerConnection[]
|
||||
notifications: {
|
||||
current: Notification | null;
|
||||
queue: Notification[];
|
||||
};
|
||||
isInOverageMode: boolean;
|
||||
isTeamOrEnterprise: boolean;
|
||||
apiKeyStatus: VerificationStatus;
|
||||
debug: boolean;
|
||||
verbose: boolean;
|
||||
tokenUsage: number;
|
||||
mainLoopModel: string;
|
||||
shouldShowAutoUpdater: boolean;
|
||||
autoUpdaterResult: AutoUpdaterResult | null;
|
||||
isAutoUpdating: boolean;
|
||||
isShowingCompactMessage: boolean;
|
||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void;
|
||||
current: Notification | null
|
||||
queue: Notification[]
|
||||
}
|
||||
isInOverageMode: boolean
|
||||
isTeamOrEnterprise: boolean
|
||||
apiKeyStatus: VerificationStatus
|
||||
debug: boolean
|
||||
verbose: boolean
|
||||
tokenUsage: number
|
||||
mainLoopModel: string
|
||||
shouldShowAutoUpdater: boolean
|
||||
autoUpdaterResult: AutoUpdaterResult | null
|
||||
isAutoUpdating: boolean
|
||||
isShowingCompactMessage: boolean
|
||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void
|
||||
}): ReactNode {
|
||||
// Poll apiKeyHelper inflight state to show slow-helper notice.
|
||||
// Gated on configuration — most users never set apiKeyHelper, so the
|
||||
// effect is a no-op for them (no interval allocated).
|
||||
const [apiKeyHelperSlow, setApiKeyHelperSlow] = useState<string | null>(null);
|
||||
const [apiKeyHelperSlow, setApiKeyHelperSlow] = useState<string | null>(null)
|
||||
useEffect(() => {
|
||||
if (!getConfiguredApiKeyHelper()) return;
|
||||
const interval = setInterval((setSlow: React.Dispatch<React.SetStateAction<string | null>>) => {
|
||||
const ms = getApiKeyHelperElapsedMs();
|
||||
const next = ms >= 10_000 ? formatDuration(ms) : null;
|
||||
setSlow(prev => next === prev ? prev : next);
|
||||
}, 1000, setApiKeyHelperSlow);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
if (!getConfiguredApiKeyHelper()) return
|
||||
const interval = setInterval(
|
||||
(setSlow: React.Dispatch<React.SetStateAction<string | null>>) => {
|
||||
const ms = getApiKeyHelperElapsedMs()
|
||||
const next = ms >= 10_000 ? formatDuration(ms) : null
|
||||
setSlow(prev => (next === prev ? prev : next))
|
||||
},
|
||||
1000,
|
||||
setApiKeyHelperSlow,
|
||||
)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
// Voice state (VOICE_MODE builds only, runtime-gated by GrowthBook)
|
||||
const voiceState = feature('VOICE_MODE') ?
|
||||
const voiceState = feature('VOICE_MODE')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useVoiceState(s => s.voiceState)
|
||||
: ('idle' as const)
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useVoiceState(s => s.voiceState) : 'idle' as const;
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false;
|
||||
const voiceError = feature('VOICE_MODE') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useVoiceState(s_0 => s_0.voiceError) : null;
|
||||
const isBriefOnly = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s_1 => s_1.isBriefOnly) : false;
|
||||
const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false
|
||||
const voiceError = feature('VOICE_MODE')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useVoiceState(s => s.voiceError)
|
||||
: null
|
||||
const isBriefOnly =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s => s.isBriefOnly)
|
||||
: false
|
||||
|
||||
// When voice is actively recording or processing, replace all
|
||||
// notifications with just the voice indicator.
|
||||
if (feature('VOICE_MODE') && voiceEnabled && (voiceState === 'recording' || voiceState === 'processing')) {
|
||||
return <VoiceIndicator voiceState={voiceState} />;
|
||||
if (
|
||||
feature('VOICE_MODE') &&
|
||||
voiceEnabled &&
|
||||
(voiceState === 'recording' || voiceState === 'processing')
|
||||
) {
|
||||
return <VoiceIndicator voiceState={voiceState} />
|
||||
}
|
||||
return <>
|
||||
|
||||
return (
|
||||
<>
|
||||
<IdeStatusIndicator ideSelection={ideSelection} mcpClients={mcpClients} />
|
||||
{notifications.current && ('jsx' in notifications.current ? <Text wrap="truncate" key={notifications.current.key}>
|
||||
{notifications.current &&
|
||||
('jsx' in notifications.current ? (
|
||||
<Text wrap="truncate" key={notifications.current.key}>
|
||||
{notifications.current.jsx}
|
||||
</Text> : <Text color={notifications.current.color} dimColor={!notifications.current.color} wrap="truncate">
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
color={notifications.current.color}
|
||||
dimColor={!notifications.current.color}
|
||||
wrap="truncate"
|
||||
>
|
||||
{notifications.current.text}
|
||||
</Text>)}
|
||||
{isInOverageMode && !isTeamOrEnterprise && <Box>
|
||||
</Text>
|
||||
))}
|
||||
{isInOverageMode && !isTeamOrEnterprise && (
|
||||
<Box>
|
||||
<Text dimColor wrap="truncate">
|
||||
Now using extra usage
|
||||
</Text>
|
||||
</Box>}
|
||||
{apiKeyHelperSlow && <Box>
|
||||
</Box>
|
||||
)}
|
||||
{apiKeyHelperSlow && (
|
||||
<Box>
|
||||
<Text color="warning" wrap="truncate">
|
||||
apiKeyHelper is taking a while{' '}
|
||||
</Text>
|
||||
<Text dimColor wrap="truncate">
|
||||
({apiKeyHelperSlow})
|
||||
</Text>
|
||||
</Box>}
|
||||
{(apiKeyStatus === 'invalid' || apiKeyStatus === 'missing') && <Box>
|
||||
</Box>
|
||||
)}
|
||||
{(apiKeyStatus === 'invalid' || apiKeyStatus === 'missing') && (
|
||||
<Box>
|
||||
<Text color="error" wrap="truncate">
|
||||
{isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ? 'Authentication error · Try again' : 'Not logged in · Run /login'}
|
||||
{isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)
|
||||
? 'Authentication error · Try again'
|
||||
: 'Not logged in · Run /login'}
|
||||
</Text>
|
||||
</Box>}
|
||||
{debug && <Box>
|
||||
</Box>
|
||||
)}
|
||||
{debug && (
|
||||
<Box>
|
||||
<Text color="warning" wrap="truncate">
|
||||
Debug mode
|
||||
</Text>
|
||||
</Box>}
|
||||
{apiKeyStatus !== 'invalid' && apiKeyStatus !== 'missing' && verbose && <Box>
|
||||
</Box>
|
||||
)}
|
||||
{apiKeyStatus !== 'invalid' && apiKeyStatus !== 'missing' && verbose && (
|
||||
<Box>
|
||||
<Text dimColor wrap="truncate">
|
||||
{tokenUsage} tokens
|
||||
</Text>
|
||||
</Box>}
|
||||
{!isBriefOnly && <TokenWarning tokenUsage={tokenUsage} model={mainLoopModel} />}
|
||||
{shouldShowAutoUpdater && <AutoUpdaterWrapper verbose={verbose} onAutoUpdaterResult={onAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} isUpdating={isAutoUpdating} onChangeIsUpdating={onChangeIsUpdating} showSuccessMessage={!isShowingCompactMessage} />}
|
||||
{feature('VOICE_MODE') ? voiceEnabled && voiceError && <Box>
|
||||
</Box>
|
||||
)}
|
||||
{!isBriefOnly && (
|
||||
<TokenWarning tokenUsage={tokenUsage} model={mainLoopModel} />
|
||||
)}
|
||||
{shouldShowAutoUpdater && (
|
||||
<AutoUpdaterWrapper
|
||||
verbose={verbose}
|
||||
onAutoUpdaterResult={onAutoUpdaterResult}
|
||||
autoUpdaterResult={autoUpdaterResult}
|
||||
isUpdating={isAutoUpdating}
|
||||
onChangeIsUpdating={onChangeIsUpdating}
|
||||
showSuccessMessage={!isShowingCompactMessage}
|
||||
/>
|
||||
)}
|
||||
{feature('VOICE_MODE')
|
||||
? voiceEnabled &&
|
||||
voiceError && (
|
||||
<Box>
|
||||
<Text color="error" wrap="truncate">
|
||||
{voiceError}
|
||||
</Text>
|
||||
</Box> : null}
|
||||
</Box>
|
||||
)
|
||||
: null}
|
||||
<MemoryUsageIndicator />
|
||||
<SandboxPromptFooterHint />
|
||||
</>;
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +1,77 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import { memo, type ReactNode, useMemo, useRef } from 'react';
|
||||
import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js';
|
||||
import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js';
|
||||
import { useSetPromptOverlay } from '../../context/promptOverlayContext.js';
|
||||
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js';
|
||||
import type { IDESelection } from '../../hooks/useIdeSelection.js';
|
||||
import { useSettings } from '../../hooks/useSettings.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { MCPServerConnection } from '../../services/mcp/types.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import type { ToolPermissionContext } from '../../Tool.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js';
|
||||
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js';
|
||||
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js';
|
||||
import { isUndercover } from '../../utils/undercover.js';
|
||||
import { CoordinatorTaskPanel, useCoordinatorTaskCount } from '../CoordinatorAgentStatus.js';
|
||||
import { getLastAssistantMessageId, StatusLine, statusLineShouldDisplay } from '../StatusLine.js';
|
||||
import { Notifications } from './Notifications.js';
|
||||
import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js';
|
||||
import { PromptInputFooterSuggestions, type SuggestionItem } from './PromptInputFooterSuggestions.js';
|
||||
import { PromptInputHelpMenu } from './PromptInputHelpMenu.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import { memo, type ReactNode, useMemo, useRef } from 'react'
|
||||
import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js'
|
||||
import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js'
|
||||
import { useSetPromptOverlay } from '../../context/promptOverlayContext.js'
|
||||
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
|
||||
import type { IDESelection } from '../../hooks/useIdeSelection.js'
|
||||
import { useSettings } from '../../hooks/useSettings.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { MCPServerConnection } from '../../services/mcp/types.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import type { ToolPermissionContext } from '../../Tool.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js'
|
||||
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
|
||||
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
|
||||
import { isUndercover } from '../../utils/undercover.js'
|
||||
import {
|
||||
CoordinatorTaskPanel,
|
||||
useCoordinatorTaskCount,
|
||||
} from '../CoordinatorAgentStatus.js'
|
||||
import {
|
||||
getLastAssistantMessageId,
|
||||
StatusLine,
|
||||
statusLineShouldDisplay,
|
||||
} from '../StatusLine.js'
|
||||
import { Notifications } from './Notifications.js'
|
||||
import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js'
|
||||
import {
|
||||
PromptInputFooterSuggestions,
|
||||
type SuggestionItem,
|
||||
} from './PromptInputFooterSuggestions.js'
|
||||
import { PromptInputHelpMenu } from './PromptInputHelpMenu.js'
|
||||
|
||||
type Props = {
|
||||
apiKeyStatus: VerificationStatus;
|
||||
debug: boolean;
|
||||
apiKeyStatus: VerificationStatus
|
||||
debug: boolean
|
||||
exitMessage: {
|
||||
show: boolean;
|
||||
key?: string;
|
||||
};
|
||||
vimMode: VimMode | undefined;
|
||||
mode: PromptInputMode;
|
||||
autoUpdaterResult: AutoUpdaterResult | null;
|
||||
isAutoUpdating: boolean;
|
||||
verbose: boolean;
|
||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void;
|
||||
suggestions: SuggestionItem[];
|
||||
selectedSuggestion: number;
|
||||
maxColumnWidth?: number;
|
||||
toolPermissionContext: ToolPermissionContext;
|
||||
helpOpen: boolean;
|
||||
suppressHint: boolean;
|
||||
isLoading: boolean;
|
||||
tasksSelected: boolean;
|
||||
teamsSelected: boolean;
|
||||
bridgeSelected: boolean;
|
||||
tmuxSelected: boolean;
|
||||
teammateFooterIndex?: number;
|
||||
ideSelection: IDESelection | undefined;
|
||||
mcpClients?: MCPServerConnection[];
|
||||
isPasting?: boolean;
|
||||
isInputWrapped?: boolean;
|
||||
messages: Message[];
|
||||
isSearching: boolean;
|
||||
historyQuery: string;
|
||||
setHistoryQuery: (query: string) => void;
|
||||
historyFailedMatch: boolean;
|
||||
onOpenTasksDialog?: (taskId?: string) => void;
|
||||
};
|
||||
show: boolean
|
||||
key?: string
|
||||
}
|
||||
vimMode: VimMode | undefined
|
||||
mode: PromptInputMode
|
||||
autoUpdaterResult: AutoUpdaterResult | null
|
||||
isAutoUpdating: boolean
|
||||
verbose: boolean
|
||||
onAutoUpdaterResult: (result: AutoUpdaterResult) => void
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void
|
||||
suggestions: SuggestionItem[]
|
||||
selectedSuggestion: number
|
||||
maxColumnWidth?: number
|
||||
toolPermissionContext: ToolPermissionContext
|
||||
helpOpen: boolean
|
||||
suppressHint: boolean
|
||||
isLoading: boolean
|
||||
tasksSelected: boolean
|
||||
teamsSelected: boolean
|
||||
bridgeSelected: boolean
|
||||
tmuxSelected: boolean
|
||||
teammateFooterIndex?: number
|
||||
ideSelection: IDESelection | undefined
|
||||
mcpClients?: MCPServerConnection[]
|
||||
isPasting?: boolean
|
||||
isInputWrapped?: boolean
|
||||
messages: Message[]
|
||||
isSearching: boolean
|
||||
historyQuery: string
|
||||
setHistoryQuery: (query: string) => void
|
||||
historyFailedMatch: boolean
|
||||
onOpenTasksDialog?: (taskId?: string) => void
|
||||
}
|
||||
|
||||
function PromptInputFooter({
|
||||
apiKeyStatus,
|
||||
debug,
|
||||
@@ -92,99 +104,176 @@ function PromptInputFooter({
|
||||
historyQuery,
|
||||
setHistoryQuery,
|
||||
historyFailedMatch,
|
||||
onOpenTasksDialog
|
||||
onOpenTasksDialog,
|
||||
}: Props): ReactNode {
|
||||
const settings = useSettings();
|
||||
const {
|
||||
columns,
|
||||
rows
|
||||
} = useTerminalSize();
|
||||
const messagesRef = useRef(messages);
|
||||
messagesRef.current = messages;
|
||||
const lastAssistantMessageId = useMemo(() => getLastAssistantMessageId(messages), [messages]);
|
||||
const isNarrow = columns < 80;
|
||||
const settings = useSettings()
|
||||
const { columns, rows } = useTerminalSize()
|
||||
const messagesRef = useRef(messages)
|
||||
messagesRef.current = messages
|
||||
const lastAssistantMessageId = useMemo(
|
||||
() => getLastAssistantMessageId(messages),
|
||||
[messages],
|
||||
)
|
||||
const isNarrow = columns < 80
|
||||
// In fullscreen the bottom slot is flexShrink:0, so every row here is a row
|
||||
// stolen from the ScrollBox. Drop the optional StatusLine first. Non-fullscreen
|
||||
// has terminal scrollback to absorb overflow, so we never hide StatusLine there.
|
||||
const isFullscreen = isFullscreenEnvEnabled();
|
||||
const isShort = isFullscreen && rows < 24;
|
||||
const isFullscreen = isFullscreenEnvEnabled()
|
||||
const isShort = isFullscreen && rows < 24
|
||||
|
||||
// Pill highlights when tasks is the active footer item AND no specific
|
||||
// agent row is selected. When coordinatorTaskIndex >= 0 the pointer has
|
||||
// moved into CoordinatorTaskPanel, so the pill should un-highlight.
|
||||
// coordinatorTaskCount === 0 covers the bash-only case (no agent rows
|
||||
// exist, pill is the only selectable item).
|
||||
const coordinatorTaskCount = useCoordinatorTaskCount();
|
||||
const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex);
|
||||
const pillSelected = tasksSelected && (coordinatorTaskCount === 0 || coordinatorTaskIndex < 0);
|
||||
const coordinatorTaskCount = useCoordinatorTaskCount()
|
||||
const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex)
|
||||
const pillSelected =
|
||||
tasksSelected && (coordinatorTaskCount === 0 || coordinatorTaskIndex < 0)
|
||||
|
||||
// Hide `? for shortcuts` if the user has a custom status line, or during ctrl-r
|
||||
const suppressHint = suppressHintFromProps || statusLineShouldDisplay(settings) || isSearching;
|
||||
const suppressHint =
|
||||
suppressHintFromProps || statusLineShouldDisplay(settings) || isSearching
|
||||
// Fullscreen: portal data to FullscreenLayout — see promptOverlayContext.tsx
|
||||
const overlayData = useMemo(() => isFullscreen && suggestions.length ? {
|
||||
suggestions,
|
||||
selectedSuggestion,
|
||||
maxColumnWidth
|
||||
} : null, [isFullscreen, suggestions, selectedSuggestion, maxColumnWidth]);
|
||||
useSetPromptOverlay(overlayData);
|
||||
const overlayData = useMemo(
|
||||
() =>
|
||||
isFullscreen && suggestions.length
|
||||
? { suggestions, selectedSuggestion, maxColumnWidth }
|
||||
: null,
|
||||
[isFullscreen, suggestions, selectedSuggestion, maxColumnWidth],
|
||||
)
|
||||
useSetPromptOverlay(overlayData)
|
||||
|
||||
if (suggestions.length && !isFullscreen) {
|
||||
return <Box paddingX={2} paddingY={0}>
|
||||
<PromptInputFooterSuggestions suggestions={suggestions} selectedSuggestion={selectedSuggestion} maxColumnWidth={maxColumnWidth} />
|
||||
</Box>;
|
||||
return (
|
||||
<Box paddingX={2} paddingY={0}>
|
||||
<PromptInputFooterSuggestions
|
||||
suggestions={suggestions}
|
||||
selectedSuggestion={selectedSuggestion}
|
||||
maxColumnWidth={maxColumnWidth}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (helpOpen) {
|
||||
return <PromptInputHelpMenu dimColor={true} fixedWidth={true} paddingX={2} />;
|
||||
return (
|
||||
<PromptInputHelpMenu dimColor={true} fixedWidth={true} paddingX={2} />
|
||||
)
|
||||
}
|
||||
return <>
|
||||
<Box flexDirection={isNarrow ? 'column' : 'row'} justifyContent={isNarrow ? 'flex-start' : 'space-between'} paddingX={2} gap={isNarrow ? 0 : 1}>
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
flexDirection={isNarrow ? 'column' : 'row'}
|
||||
justifyContent={isNarrow ? 'flex-start' : 'space-between'}
|
||||
paddingX={2}
|
||||
gap={isNarrow ? 0 : 1}
|
||||
>
|
||||
<Box flexDirection="column" flexShrink={isNarrow ? 0 : 1}>
|
||||
{mode === 'prompt' && !isShort && !exitMessage.show && !isPasting && statusLineShouldDisplay(settings) && <StatusLine messagesRef={messagesRef} lastAssistantMessageId={lastAssistantMessageId} vimMode={vimMode} />}
|
||||
<PromptInputFooterLeftSide exitMessage={exitMessage} vimMode={vimMode} mode={mode} toolPermissionContext={toolPermissionContext} suppressHint={suppressHint} isLoading={isLoading} tasksSelected={pillSelected} teamsSelected={teamsSelected} teammateFooterIndex={teammateFooterIndex} tmuxSelected={tmuxSelected} isPasting={isPasting} isSearching={isSearching} historyQuery={historyQuery} setHistoryQuery={setHistoryQuery} historyFailedMatch={historyFailedMatch} onOpenTasksDialog={onOpenTasksDialog} />
|
||||
{mode === 'prompt' &&
|
||||
!isShort &&
|
||||
!exitMessage.show &&
|
||||
!isPasting &&
|
||||
statusLineShouldDisplay(settings) && (
|
||||
<StatusLine
|
||||
messagesRef={messagesRef}
|
||||
lastAssistantMessageId={lastAssistantMessageId}
|
||||
vimMode={vimMode}
|
||||
/>
|
||||
)}
|
||||
<PromptInputFooterLeftSide
|
||||
exitMessage={exitMessage}
|
||||
vimMode={vimMode}
|
||||
mode={mode}
|
||||
toolPermissionContext={toolPermissionContext}
|
||||
suppressHint={suppressHint}
|
||||
isLoading={isLoading}
|
||||
tasksSelected={pillSelected}
|
||||
teamsSelected={teamsSelected}
|
||||
teammateFooterIndex={teammateFooterIndex}
|
||||
tmuxSelected={tmuxSelected}
|
||||
isPasting={isPasting}
|
||||
isSearching={isSearching}
|
||||
historyQuery={historyQuery}
|
||||
setHistoryQuery={setHistoryQuery}
|
||||
historyFailedMatch={historyFailedMatch}
|
||||
onOpenTasksDialog={onOpenTasksDialog}
|
||||
/>
|
||||
</Box>
|
||||
<Box flexShrink={1} gap={1}>
|
||||
{isFullscreen ? null : <Notifications apiKeyStatus={apiKeyStatus} autoUpdaterResult={autoUpdaterResult} debug={debug} isAutoUpdating={isAutoUpdating} verbose={verbose} messages={messages} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={onChangeIsUpdating} ideSelection={ideSelection} mcpClients={mcpClients} isInputWrapped={isInputWrapped} isNarrow={isNarrow} />}
|
||||
{(process.env.USER_TYPE) === 'ant' && isUndercover() && <Text dimColor>undercover</Text>}
|
||||
{isFullscreen ? null : (
|
||||
<Notifications
|
||||
apiKeyStatus={apiKeyStatus}
|
||||
autoUpdaterResult={autoUpdaterResult}
|
||||
debug={debug}
|
||||
isAutoUpdating={isAutoUpdating}
|
||||
verbose={verbose}
|
||||
messages={messages}
|
||||
onAutoUpdaterResult={onAutoUpdaterResult}
|
||||
onChangeIsUpdating={onChangeIsUpdating}
|
||||
ideSelection={ideSelection}
|
||||
mcpClients={mcpClients}
|
||||
isInputWrapped={isInputWrapped}
|
||||
isNarrow={isNarrow}
|
||||
/>
|
||||
)}
|
||||
{process.env.USER_TYPE === 'ant' && isUndercover() && (
|
||||
<Text dimColor>undercover</Text>
|
||||
)}
|
||||
<BridgeStatusIndicator bridgeSelected={bridgeSelected} />
|
||||
</Box>
|
||||
</Box>
|
||||
{(process.env.USER_TYPE) === 'ant' && <CoordinatorTaskPanel />}
|
||||
</>;
|
||||
{process.env.USER_TYPE === 'ant' && <CoordinatorTaskPanel />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default memo(PromptInputFooter);
|
||||
|
||||
export default memo(PromptInputFooter)
|
||||
|
||||
type BridgeStatusProps = {
|
||||
bridgeSelected: boolean;
|
||||
};
|
||||
bridgeSelected: boolean
|
||||
}
|
||||
|
||||
function BridgeStatusIndicator({
|
||||
bridgeSelected
|
||||
bridgeSelected,
|
||||
}: BridgeStatusProps): React.ReactNode {
|
||||
if (!feature('BRIDGE_MODE')) return null;
|
||||
if (!feature('BRIDGE_MODE')) return null
|
||||
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
const enabled = useAppState(s => s.replBridgeEnabled);
|
||||
const enabled = useAppState(s => s.replBridgeEnabled)
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
const connected = useAppState(s_0 => s_0.replBridgeConnected);
|
||||
const connected = useAppState(s => s.replBridgeConnected)
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
const sessionActive = useAppState(s_1 => s_1.replBridgeSessionActive);
|
||||
const sessionActive = useAppState(s => s.replBridgeSessionActive)
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
const reconnecting = useAppState(s_2 => s_2.replBridgeReconnecting);
|
||||
const reconnecting = useAppState(s => s.replBridgeReconnecting)
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
const explicit = useAppState(s_3 => s_3.replBridgeExplicit);
|
||||
const explicit = useAppState(s => s.replBridgeExplicit)
|
||||
|
||||
// Failed state is surfaced via notification (useReplBridge), not a footer pill.
|
||||
if (!isBridgeEnabled() || !enabled) return null;
|
||||
if (!isBridgeEnabled() || !enabled) return null
|
||||
|
||||
const status = getBridgeStatus({
|
||||
error: undefined,
|
||||
connected,
|
||||
sessionActive,
|
||||
reconnecting
|
||||
});
|
||||
reconnecting,
|
||||
})
|
||||
|
||||
// For implicit (config-driven) remote, only show the reconnecting state
|
||||
if (!explicit && status.label !== 'Remote Control reconnecting') {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return <Text color={bridgeSelected ? 'background' : status.color} inverse={bridgeSelected} wrap="truncate">
|
||||
|
||||
return (
|
||||
<Text
|
||||
color={bridgeSelected ? 'background' : status.color}
|
||||
inverse={bridgeSelected}
|
||||
wrap="truncate"
|
||||
>
|
||||
{status.label}
|
||||
{bridgeSelected && <Text dimColor> · Enter to view</Text>}
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,292 +1,248 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { memo, type ReactNode } from 'react';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
import * as React from 'react'
|
||||
import { memo, type ReactNode } from 'react'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js'
|
||||
import type { Theme } from '../../utils/theme.js'
|
||||
|
||||
export type SuggestionItem = {
|
||||
id: string;
|
||||
displayText: string;
|
||||
tag?: string;
|
||||
description?: string;
|
||||
metadata?: unknown;
|
||||
color?: keyof Theme;
|
||||
};
|
||||
export type SuggestionType = 'command' | 'file' | 'directory' | 'agent' | 'shell' | 'custom-title' | 'slack-channel' | 'none';
|
||||
export const OVERLAY_MAX_ITEMS = 5;
|
||||
id: string
|
||||
displayText: string
|
||||
tag?: string
|
||||
description?: string
|
||||
metadata?: unknown
|
||||
color?: keyof Theme
|
||||
}
|
||||
|
||||
export type SuggestionType =
|
||||
| 'command'
|
||||
| 'file'
|
||||
| 'directory'
|
||||
| 'agent'
|
||||
| 'shell'
|
||||
| 'custom-title'
|
||||
| 'slack-channel'
|
||||
| 'none'
|
||||
|
||||
export const OVERLAY_MAX_ITEMS = 5
|
||||
|
||||
/**
|
||||
* Get the icon for a suggestion based on its type
|
||||
* Icons: + for files, ◇ for MCP resources, * for agents
|
||||
*/
|
||||
function getIcon(itemId: string): string {
|
||||
if (itemId.startsWith('file-')) return '+';
|
||||
if (itemId.startsWith('mcp-resource-')) return '◇';
|
||||
if (itemId.startsWith('agent-')) return '*';
|
||||
return '+';
|
||||
if (itemId.startsWith('file-')) return '+'
|
||||
if (itemId.startsWith('mcp-resource-')) return '◇'
|
||||
if (itemId.startsWith('agent-')) return '*'
|
||||
return '+'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an item is a unified suggestion type (file, mcp-resource, or agent)
|
||||
*/
|
||||
function isUnifiedSuggestion(itemId: string): boolean {
|
||||
return itemId.startsWith('file-') || itemId.startsWith('mcp-resource-') || itemId.startsWith('agent-');
|
||||
return (
|
||||
itemId.startsWith('file-') ||
|
||||
itemId.startsWith('mcp-resource-') ||
|
||||
itemId.startsWith('agent-')
|
||||
)
|
||||
}
|
||||
const SuggestionItemRow = memo(function SuggestionItemRow(t0: { item: SuggestionItem; maxColumnWidth: number; isSelected: boolean }) {
|
||||
const $ = _c(36);
|
||||
const {
|
||||
item,
|
||||
maxColumnWidth,
|
||||
isSelected
|
||||
} = t0;
|
||||
const columns = useTerminalSize().columns;
|
||||
const isUnified = isUnifiedSuggestion(item.id);
|
||||
|
||||
const SuggestionItemRow = memo(function SuggestionItemRow({
|
||||
item,
|
||||
maxColumnWidth,
|
||||
isSelected,
|
||||
}: {
|
||||
item: SuggestionItem
|
||||
maxColumnWidth?: number
|
||||
isSelected: boolean
|
||||
}): ReactNode {
|
||||
const columns = useTerminalSize().columns
|
||||
const isUnified = isUnifiedSuggestion(item.id)
|
||||
|
||||
// For unified suggestions (file, mcp-resource, agent), use single-line layout with icon
|
||||
if (isUnified) {
|
||||
let t1;
|
||||
if ($[0] !== item.id) {
|
||||
t1 = getIcon(item.id);
|
||||
$[0] = item.id;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const icon = t1;
|
||||
const textColor = isSelected ? "suggestion" : undefined;
|
||||
const dimColor = !isSelected;
|
||||
const isFile = item.id.startsWith("file-");
|
||||
const isMcpResource = item.id.startsWith("mcp-resource-");
|
||||
const separatorWidth = item.description ? 3 : 0;
|
||||
let displayText;
|
||||
const icon = getIcon(item.id)
|
||||
const textColor: keyof Theme | undefined = isSelected
|
||||
? 'suggestion'
|
||||
: undefined
|
||||
const dimColor = !isSelected
|
||||
|
||||
const isFile = item.id.startsWith('file-')
|
||||
const isMcpResource = item.id.startsWith('mcp-resource-')
|
||||
|
||||
// Calculate layout widths
|
||||
// Layout: "X " (2) + displayText + " – " (3) + description + padding (4)
|
||||
const iconWidth = 2 // icon + space (fixed)
|
||||
const paddingWidth = 4
|
||||
const separatorWidth = item.description ? 3 : 0 // ' – ' separator
|
||||
|
||||
// For files, truncate middle of path to show both directory context and filename
|
||||
// For MCP resources, limit displayText to 30 chars (truncate from end)
|
||||
// For agents, no truncation
|
||||
let displayText: string
|
||||
if (isFile) {
|
||||
let t2;
|
||||
if ($[2] !== item.description) {
|
||||
t2 = item.description ? Math.min(20, stringWidth(item.description)) : 0;
|
||||
$[2] = item.description;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const descReserve = t2;
|
||||
const maxPathLength = columns - 2 - 4 - separatorWidth - descReserve;
|
||||
let t3;
|
||||
if ($[4] !== item.displayText || $[5] !== maxPathLength) {
|
||||
t3 = truncatePathMiddle(item.displayText, maxPathLength);
|
||||
$[4] = item.displayText;
|
||||
$[5] = maxPathLength;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
displayText = t3;
|
||||
// Reserve space for description if present, otherwise use all available space
|
||||
const descReserve = item.description
|
||||
? Math.min(20, stringWidth(item.description))
|
||||
: 0
|
||||
const maxPathLength =
|
||||
columns - iconWidth - paddingWidth - separatorWidth - descReserve
|
||||
displayText = truncatePathMiddle(item.displayText, maxPathLength)
|
||||
} else if (isMcpResource) {
|
||||
const maxDisplayTextLength = 30
|
||||
displayText = truncateToWidth(item.displayText, maxDisplayTextLength)
|
||||
} else {
|
||||
if (isMcpResource) {
|
||||
let t2;
|
||||
if ($[7] !== item.displayText) {
|
||||
t2 = truncateToWidth(item.displayText, 30);
|
||||
$[7] = item.displayText;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
displayText = t2;
|
||||
} else {
|
||||
displayText = item.displayText;
|
||||
}
|
||||
displayText = item.displayText
|
||||
}
|
||||
const availableWidth = columns - 2 - stringWidth(displayText) - separatorWidth - 4;
|
||||
let lineContent;
|
||||
|
||||
const availableWidth =
|
||||
columns -
|
||||
iconWidth -
|
||||
stringWidth(displayText) -
|
||||
separatorWidth -
|
||||
paddingWidth
|
||||
|
||||
// Build the full line as a single string to prevent wrapping
|
||||
let lineContent: string
|
||||
if (item.description) {
|
||||
const maxDescLength = Math.max(0, availableWidth);
|
||||
let t2;
|
||||
if ($[9] !== item.description || $[10] !== maxDescLength) {
|
||||
t2 = truncateToWidth(item.description.replace(/\s+/g, " "), maxDescLength);
|
||||
$[9] = item.description;
|
||||
$[10] = maxDescLength;
|
||||
$[11] = t2;
|
||||
} else {
|
||||
t2 = $[11];
|
||||
}
|
||||
const truncatedDesc = t2;
|
||||
lineContent = `${icon} ${displayText} – ${truncatedDesc}`;
|
||||
const maxDescLength = Math.max(0, availableWidth)
|
||||
const truncatedDesc = truncateToWidth(
|
||||
item.description.replace(/\s+/g, ' '),
|
||||
maxDescLength,
|
||||
)
|
||||
lineContent = `${icon} ${displayText} – ${truncatedDesc}`
|
||||
} else {
|
||||
lineContent = `${icon} ${displayText}`;
|
||||
lineContent = `${icon} ${displayText}`
|
||||
}
|
||||
let t2;
|
||||
if ($[12] !== dimColor || $[13] !== lineContent || $[14] !== textColor) {
|
||||
t2 = <Text color={textColor} dimColor={dimColor} wrap="truncate">{lineContent}</Text>;
|
||||
$[12] = dimColor;
|
||||
$[13] = lineContent;
|
||||
$[14] = textColor;
|
||||
$[15] = t2;
|
||||
} else {
|
||||
t2 = $[15];
|
||||
}
|
||||
return t2;
|
||||
|
||||
return (
|
||||
<Text color={textColor} dimColor={dimColor} wrap="truncate">
|
||||
{lineContent}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
const maxNameWidth = Math.floor(columns * 0.4);
|
||||
const displayTextWidth = Math.min(maxColumnWidth ?? stringWidth(item.displayText) + 5, maxNameWidth);
|
||||
const textColor_0 = item.color || (isSelected ? "suggestion" : undefined);
|
||||
const shouldDim = !isSelected;
|
||||
let displayText_0 = item.displayText;
|
||||
if (stringWidth(displayText_0) > displayTextWidth - 2) {
|
||||
const t1 = displayTextWidth - 2;
|
||||
let t2;
|
||||
if ($[16] !== displayText_0 || $[17] !== t1) {
|
||||
t2 = truncateToWidth(displayText_0, t1);
|
||||
$[16] = displayText_0;
|
||||
$[17] = t1;
|
||||
$[18] = t2;
|
||||
} else {
|
||||
t2 = $[18];
|
||||
}
|
||||
displayText_0 = t2;
|
||||
|
||||
// For non-unified suggestions (commands, shell, etc.), use improved layout from main
|
||||
// Cap the command name column at 40% of terminal width to ensure description has space
|
||||
const maxNameWidth = Math.floor(columns * 0.4)
|
||||
const displayTextWidth = Math.min(
|
||||
maxColumnWidth ?? stringWidth(item.displayText) + 5,
|
||||
maxNameWidth,
|
||||
)
|
||||
|
||||
const textColor = item.color || (isSelected ? 'suggestion' : undefined)
|
||||
const shouldDim = !isSelected
|
||||
|
||||
// Truncate and pad the display text to fixed width
|
||||
let displayText = item.displayText
|
||||
if (stringWidth(displayText) > displayTextWidth - 2) {
|
||||
displayText = truncateToWidth(displayText, displayTextWidth - 2)
|
||||
}
|
||||
const paddedDisplayText = displayText_0 + " ".repeat(Math.max(0, displayTextWidth - stringWidth(displayText_0)));
|
||||
const tagText = item.tag ? `[${item.tag}] ` : "";
|
||||
const tagWidth = stringWidth(tagText);
|
||||
const descriptionWidth = Math.max(0, columns - displayTextWidth - tagWidth - 4);
|
||||
let t1;
|
||||
if ($[19] !== descriptionWidth || $[20] !== item.description) {
|
||||
t1 = item.description ? truncateToWidth(item.description.replace(/\s+/g, " "), descriptionWidth) : "";
|
||||
$[19] = descriptionWidth;
|
||||
$[20] = item.description;
|
||||
$[21] = t1;
|
||||
} else {
|
||||
t1 = $[21];
|
||||
}
|
||||
const truncatedDescription = t1;
|
||||
let t2;
|
||||
if ($[22] !== paddedDisplayText || $[23] !== shouldDim || $[24] !== textColor_0) {
|
||||
t2 = <Text color={textColor_0} dimColor={shouldDim}>{paddedDisplayText}</Text>;
|
||||
$[22] = paddedDisplayText;
|
||||
$[23] = shouldDim;
|
||||
$[24] = textColor_0;
|
||||
$[25] = t2;
|
||||
} else {
|
||||
t2 = $[25];
|
||||
}
|
||||
let t3;
|
||||
if ($[26] !== tagText) {
|
||||
t3 = tagText ? <Text dimColor={true}>{tagText}</Text> : null;
|
||||
$[26] = tagText;
|
||||
$[27] = t3;
|
||||
} else {
|
||||
t3 = $[27];
|
||||
}
|
||||
const t4 = isSelected ? "suggestion" : undefined;
|
||||
const t5 = !isSelected;
|
||||
let t6;
|
||||
if ($[28] !== t4 || $[29] !== t5 || $[30] !== truncatedDescription) {
|
||||
t6 = <Text color={t4} dimColor={t5}>{truncatedDescription}</Text>;
|
||||
$[28] = t4;
|
||||
$[29] = t5;
|
||||
$[30] = truncatedDescription;
|
||||
$[31] = t6;
|
||||
} else {
|
||||
t6 = $[31];
|
||||
}
|
||||
let t7;
|
||||
if ($[32] !== t2 || $[33] !== t3 || $[34] !== t6) {
|
||||
t7 = <Text wrap="truncate">{t2}{t3}{t6}</Text>;
|
||||
$[32] = t2;
|
||||
$[33] = t3;
|
||||
$[34] = t6;
|
||||
$[35] = t7;
|
||||
} else {
|
||||
t7 = $[35];
|
||||
}
|
||||
return t7;
|
||||
});
|
||||
const paddedDisplayText =
|
||||
displayText +
|
||||
' '.repeat(Math.max(0, displayTextWidth - stringWidth(displayText)))
|
||||
|
||||
const tagText = item.tag ? `[${item.tag}] ` : ''
|
||||
const tagWidth = stringWidth(tagText)
|
||||
const descriptionWidth = Math.max(
|
||||
0,
|
||||
columns - displayTextWidth - tagWidth - 4,
|
||||
)
|
||||
// Skill descriptions can contain newlines (e.g. /claude-api's "TRIGGER
|
||||
// when:" block). A multi-line row grows the overlay past minHeight; when
|
||||
// the filter narrows past that skill, the overlay shrinks and leaves
|
||||
// ghost rows. Flatten to one line before truncating.
|
||||
const truncatedDescription = item.description
|
||||
? truncateToWidth(item.description.replace(/\s+/g, ' '), descriptionWidth)
|
||||
: ''
|
||||
|
||||
return (
|
||||
<Text wrap="truncate">
|
||||
<Text color={textColor} dimColor={shouldDim}>
|
||||
{paddedDisplayText}
|
||||
</Text>
|
||||
{tagText ? <Text dimColor>{tagText}</Text> : null}
|
||||
<Text
|
||||
color={isSelected ? 'suggestion' : undefined}
|
||||
dimColor={!isSelected}
|
||||
>
|
||||
{truncatedDescription}
|
||||
</Text>
|
||||
</Text>
|
||||
)
|
||||
})
|
||||
|
||||
type Props = {
|
||||
suggestions: SuggestionItem[];
|
||||
selectedSuggestion: number;
|
||||
maxColumnWidth?: number;
|
||||
suggestions: SuggestionItem[]
|
||||
selectedSuggestion: number
|
||||
maxColumnWidth?: number
|
||||
/**
|
||||
* When true, the suggestions are rendered inside a position=absolute
|
||||
* overlay. We omit minHeight and flex-end so the y-clamp in the
|
||||
* renderer doesn't push fewer items down into the prompt area.
|
||||
*/
|
||||
overlay?: boolean;
|
||||
};
|
||||
export function PromptInputFooterSuggestions(t0) {
|
||||
const $ = _c(22);
|
||||
const {
|
||||
suggestions,
|
||||
selectedSuggestion,
|
||||
maxColumnWidth: maxColumnWidthProp,
|
||||
overlay
|
||||
} = t0;
|
||||
const {
|
||||
rows
|
||||
} = useTerminalSize();
|
||||
const maxVisibleItems = overlay ? OVERLAY_MAX_ITEMS : Math.min(6, Math.max(1, rows - 3));
|
||||
overlay?: boolean
|
||||
}
|
||||
|
||||
export function PromptInputFooterSuggestions({
|
||||
suggestions,
|
||||
selectedSuggestion,
|
||||
maxColumnWidth: maxColumnWidthProp,
|
||||
overlay,
|
||||
}: Props): ReactNode {
|
||||
const { rows } = useTerminalSize()
|
||||
// Maximum number of suggestions to show at once (leaving space for prompt).
|
||||
// Overlay mode (fullscreen) uses a fixed 5 — the floating box sits over
|
||||
// the ScrollBox, so terminal height isn't the constraint.
|
||||
const maxVisibleItems = overlay
|
||||
? OVERLAY_MAX_ITEMS
|
||||
: Math.min(6, Math.max(1, rows - 3))
|
||||
|
||||
// No suggestions to display
|
||||
if (suggestions.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t1;
|
||||
if ($[0] !== maxColumnWidthProp || $[1] !== suggestions) {
|
||||
t1 = maxColumnWidthProp ?? Math.max(...suggestions.map(_temp)) + 5;
|
||||
$[0] = maxColumnWidthProp;
|
||||
$[1] = suggestions;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const maxColumnWidth = t1;
|
||||
const startIndex = Math.max(0, Math.min(selectedSuggestion - Math.floor(maxVisibleItems / 2), suggestions.length - maxVisibleItems));
|
||||
const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length);
|
||||
let T0;
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[3] !== endIndex || $[4] !== maxColumnWidth || $[5] !== overlay || $[6] !== selectedSuggestion || $[7] !== startIndex || $[8] !== suggestions) {
|
||||
const visibleItems = suggestions.slice(startIndex, endIndex);
|
||||
T0 = Box;
|
||||
t2 = "column";
|
||||
t3 = overlay ? undefined : "flex-end";
|
||||
let t5;
|
||||
if ($[13] !== maxColumnWidth || $[14] !== selectedSuggestion || $[15] !== suggestions) {
|
||||
t5 = item_0 => <SuggestionItemRow key={item_0.id} item={item_0} maxColumnWidth={maxColumnWidth} isSelected={item_0.id === suggestions[selectedSuggestion]?.id} />;
|
||||
$[13] = maxColumnWidth;
|
||||
$[14] = selectedSuggestion;
|
||||
$[15] = suggestions;
|
||||
$[16] = t5;
|
||||
} else {
|
||||
t5 = $[16];
|
||||
}
|
||||
t4 = visibleItems.map(t5);
|
||||
$[3] = endIndex;
|
||||
$[4] = maxColumnWidth;
|
||||
$[5] = overlay;
|
||||
$[6] = selectedSuggestion;
|
||||
$[7] = startIndex;
|
||||
$[8] = suggestions;
|
||||
$[9] = T0;
|
||||
$[10] = t2;
|
||||
$[11] = t3;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
T0 = $[9];
|
||||
t2 = $[10];
|
||||
t3 = $[11];
|
||||
t4 = $[12];
|
||||
}
|
||||
let t5;
|
||||
if ($[17] !== T0 || $[18] !== t2 || $[19] !== t3 || $[20] !== t4) {
|
||||
t5 = <T0 flexDirection={t2} justifyContent={t3}>{t4}</T0>;
|
||||
$[17] = T0;
|
||||
$[18] = t2;
|
||||
$[19] = t3;
|
||||
$[20] = t4;
|
||||
$[21] = t5;
|
||||
} else {
|
||||
t5 = $[21];
|
||||
}
|
||||
return t5;
|
||||
|
||||
// Use prop if provided (stable width from all commands), otherwise calculate from visible
|
||||
const maxColumnWidth =
|
||||
maxColumnWidthProp ??
|
||||
Math.max(...suggestions.map(item => stringWidth(item.displayText))) + 5
|
||||
|
||||
// Calculate visible items range based on selected index
|
||||
const startIndex = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
selectedSuggestion - Math.floor(maxVisibleItems / 2),
|
||||
suggestions.length - maxVisibleItems,
|
||||
),
|
||||
)
|
||||
const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length)
|
||||
const visibleItems = suggestions.slice(startIndex, endIndex)
|
||||
|
||||
// In non-overlay (inline) mode, justifyContent keeps suggestions
|
||||
// anchored to the bottom (near the prompt). In overlay mode we omit
|
||||
// both minHeight and flex-end: the parent is position=absolute with
|
||||
// bottom='100%', so its y is clamped to 0 by the renderer when it
|
||||
// would go negative. Adding minHeight + flex-end would create empty
|
||||
// padding rows that shift the visible items down into the prompt area
|
||||
// when the list has fewer items than maxVisibleItems.
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
justifyContent={overlay ? undefined : 'flex-end'}
|
||||
>
|
||||
{visibleItems.map(item => (
|
||||
<SuggestionItemRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
maxColumnWidth={maxColumnWidth}
|
||||
isSelected={item.id === suggestions[selectedSuggestion]?.id}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function _temp(item) {
|
||||
return stringWidth(item.displayText);
|
||||
}
|
||||
export default memo(PromptInputFooterSuggestions);
|
||||
|
||||
export default memo(PromptInputFooterSuggestions)
|
||||
|
||||
@@ -1,357 +1,149 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from 'src/ink.js';
|
||||
import { getPlatform } from 'src/utils/platform.js';
|
||||
import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js';
|
||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js';
|
||||
import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js';
|
||||
import { getNewlineInstructions } from './utils.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from 'src/ink.js'
|
||||
import { getPlatform } from 'src/utils/platform.js'
|
||||
import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js'
|
||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js'
|
||||
import { getNewlineInstructions } from './utils.js'
|
||||
|
||||
/** Format a shortcut for display in the help menu (e.g., "ctrl+o" → "ctrl + o") */
|
||||
function formatShortcut(shortcut: string): string {
|
||||
return shortcut.replace(/\+/g, ' + ');
|
||||
return shortcut.replace(/\+/g, ' + ')
|
||||
}
|
||||
|
||||
type Props = {
|
||||
dimColor?: boolean;
|
||||
fixedWidth?: boolean;
|
||||
gap?: number;
|
||||
paddingX?: number;
|
||||
};
|
||||
export function PromptInputHelpMenu(props) {
|
||||
const $ = _c(99);
|
||||
const {
|
||||
dimColor,
|
||||
fixedWidth,
|
||||
gap,
|
||||
paddingX
|
||||
} = props;
|
||||
const t0 = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
|
||||
let t1;
|
||||
if ($[0] !== t0) {
|
||||
t1 = formatShortcut(t0);
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const transcriptShortcut = t1;
|
||||
const t2 = useShortcutDisplay("app:toggleTodos", "Global", "ctrl+t");
|
||||
let t3;
|
||||
if ($[2] !== t2) {
|
||||
t3 = formatShortcut(t2);
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const todosShortcut = t3;
|
||||
const t4 = useShortcutDisplay("chat:undo", "Chat", "ctrl+_");
|
||||
let t5;
|
||||
if ($[4] !== t4) {
|
||||
t5 = formatShortcut(t4);
|
||||
$[4] = t4;
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
}
|
||||
const undoShortcut = t5;
|
||||
const t6 = useShortcutDisplay("chat:stash", "Chat", "ctrl+s");
|
||||
let t7;
|
||||
if ($[6] !== t6) {
|
||||
t7 = formatShortcut(t6);
|
||||
$[6] = t6;
|
||||
$[7] = t7;
|
||||
} else {
|
||||
t7 = $[7];
|
||||
}
|
||||
const stashShortcut = t7;
|
||||
const t8 = useShortcutDisplay("chat:cycleMode", "Chat", "shift+tab");
|
||||
let t9;
|
||||
if ($[8] !== t8) {
|
||||
t9 = formatShortcut(t8);
|
||||
$[8] = t8;
|
||||
$[9] = t9;
|
||||
} else {
|
||||
t9 = $[9];
|
||||
}
|
||||
const cycleModeShortcut = t9;
|
||||
const t10 = useShortcutDisplay("chat:modelPicker", "Chat", "alt+p");
|
||||
let t11;
|
||||
if ($[10] !== t10) {
|
||||
t11 = formatShortcut(t10);
|
||||
$[10] = t10;
|
||||
$[11] = t11;
|
||||
} else {
|
||||
t11 = $[11];
|
||||
}
|
||||
const modelPickerShortcut = t11;
|
||||
const t12 = useShortcutDisplay("chat:fastMode", "Chat", "alt+o");
|
||||
let t13;
|
||||
if ($[12] !== t12) {
|
||||
t13 = formatShortcut(t12);
|
||||
$[12] = t12;
|
||||
$[13] = t13;
|
||||
} else {
|
||||
t13 = $[13];
|
||||
}
|
||||
const fastModeShortcut = t13;
|
||||
const t14 = useShortcutDisplay("chat:externalEditor", "Chat", "ctrl+g");
|
||||
let t15;
|
||||
if ($[14] !== t14) {
|
||||
t15 = formatShortcut(t14);
|
||||
$[14] = t14;
|
||||
$[15] = t15;
|
||||
} else {
|
||||
t15 = $[15];
|
||||
}
|
||||
const externalEditorShortcut = t15;
|
||||
const t16 = useShortcutDisplay("app:toggleTerminal", "Global", "meta+j");
|
||||
let t17;
|
||||
if ($[16] !== t16) {
|
||||
t17 = formatShortcut(t16);
|
||||
$[16] = t16;
|
||||
$[17] = t17;
|
||||
} else {
|
||||
t17 = $[17];
|
||||
}
|
||||
const terminalShortcut = t17;
|
||||
const t18 = useShortcutDisplay("chat:imagePaste", "Chat", "ctrl+v");
|
||||
let t19;
|
||||
if ($[18] !== t18) {
|
||||
t19 = formatShortcut(t18);
|
||||
$[18] = t18;
|
||||
$[19] = t19;
|
||||
} else {
|
||||
t19 = $[19];
|
||||
}
|
||||
const imagePasteShortcut = t19;
|
||||
let t20;
|
||||
if ($[20] !== dimColor || $[21] !== terminalShortcut) {
|
||||
t20 = feature("TERMINAL_PANEL") ? getFeatureValue_CACHED_MAY_BE_STALE("tengu_terminal_panel", false) ? <Box><Text dimColor={dimColor}>{terminalShortcut} for terminal</Text></Box> : null : null;
|
||||
$[20] = dimColor;
|
||||
$[21] = terminalShortcut;
|
||||
$[22] = t20;
|
||||
} else {
|
||||
t20 = $[22];
|
||||
}
|
||||
const terminalShortcutElement = t20;
|
||||
const t21 = fixedWidth ? 24 : undefined;
|
||||
let t22;
|
||||
if ($[23] !== dimColor) {
|
||||
t22 = <Box><Text dimColor={dimColor}>! for bash mode</Text></Box>;
|
||||
$[23] = dimColor;
|
||||
$[24] = t22;
|
||||
} else {
|
||||
t22 = $[24];
|
||||
}
|
||||
let t23;
|
||||
if ($[25] !== dimColor) {
|
||||
t23 = <Box><Text dimColor={dimColor}>/ for commands</Text></Box>;
|
||||
$[25] = dimColor;
|
||||
$[26] = t23;
|
||||
} else {
|
||||
t23 = $[26];
|
||||
}
|
||||
let t24;
|
||||
if ($[27] !== dimColor) {
|
||||
t24 = <Box><Text dimColor={dimColor}>@ for file paths</Text></Box>;
|
||||
$[27] = dimColor;
|
||||
$[28] = t24;
|
||||
} else {
|
||||
t24 = $[28];
|
||||
}
|
||||
let t25;
|
||||
if ($[29] !== dimColor) {
|
||||
t25 = <Box><Text dimColor={dimColor}>{"& for background"}</Text></Box>;
|
||||
$[29] = dimColor;
|
||||
$[30] = t25;
|
||||
} else {
|
||||
t25 = $[30];
|
||||
}
|
||||
let t26;
|
||||
if ($[31] !== dimColor) {
|
||||
t26 = <Box><Text dimColor={dimColor}>/btw for side question</Text></Box>;
|
||||
$[31] = dimColor;
|
||||
$[32] = t26;
|
||||
} else {
|
||||
t26 = $[32];
|
||||
}
|
||||
let t27;
|
||||
if ($[33] !== t21 || $[34] !== t22 || $[35] !== t23 || $[36] !== t24 || $[37] !== t25 || $[38] !== t26) {
|
||||
t27 = <Box flexDirection="column" width={t21}>{t22}{t23}{t24}{t25}{t26}</Box>;
|
||||
$[33] = t21;
|
||||
$[34] = t22;
|
||||
$[35] = t23;
|
||||
$[36] = t24;
|
||||
$[37] = t25;
|
||||
$[38] = t26;
|
||||
$[39] = t27;
|
||||
} else {
|
||||
t27 = $[39];
|
||||
}
|
||||
const t28 = fixedWidth ? 35 : undefined;
|
||||
let t29;
|
||||
if ($[40] !== dimColor) {
|
||||
t29 = <Box><Text dimColor={dimColor}>double tap esc to clear input</Text></Box>;
|
||||
$[40] = dimColor;
|
||||
$[41] = t29;
|
||||
} else {
|
||||
t29 = $[41];
|
||||
}
|
||||
let t30;
|
||||
if ($[42] !== cycleModeShortcut || $[43] !== dimColor) {
|
||||
t30 = <Box><Text dimColor={dimColor}>{cycleModeShortcut}{" "}{false ? "to cycle modes" : "to auto-accept edits"}</Text></Box>;
|
||||
$[42] = cycleModeShortcut;
|
||||
$[43] = dimColor;
|
||||
$[44] = t30;
|
||||
} else {
|
||||
t30 = $[44];
|
||||
}
|
||||
let t31;
|
||||
if ($[45] !== dimColor || $[46] !== transcriptShortcut) {
|
||||
t31 = <Box><Text dimColor={dimColor}>{transcriptShortcut} for verbose output</Text></Box>;
|
||||
$[45] = dimColor;
|
||||
$[46] = transcriptShortcut;
|
||||
$[47] = t31;
|
||||
} else {
|
||||
t31 = $[47];
|
||||
}
|
||||
let t32;
|
||||
if ($[48] !== dimColor || $[49] !== todosShortcut) {
|
||||
t32 = <Box><Text dimColor={dimColor}>{todosShortcut} to toggle tasks</Text></Box>;
|
||||
$[48] = dimColor;
|
||||
$[49] = todosShortcut;
|
||||
$[50] = t32;
|
||||
} else {
|
||||
t32 = $[50];
|
||||
}
|
||||
let t33;
|
||||
if ($[51] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t33 = getNewlineInstructions();
|
||||
$[51] = t33;
|
||||
} else {
|
||||
t33 = $[51];
|
||||
}
|
||||
let t34;
|
||||
if ($[52] !== dimColor) {
|
||||
t34 = <Box><Text dimColor={dimColor}>{t33}</Text></Box>;
|
||||
$[52] = dimColor;
|
||||
$[53] = t34;
|
||||
} else {
|
||||
t34 = $[53];
|
||||
}
|
||||
let t35;
|
||||
if ($[54] !== t28 || $[55] !== t29 || $[56] !== t30 || $[57] !== t31 || $[58] !== t32 || $[59] !== t34 || $[60] !== terminalShortcutElement) {
|
||||
t35 = <Box flexDirection="column" width={t28}>{t29}{t30}{t31}{t32}{terminalShortcutElement}{t34}</Box>;
|
||||
$[54] = t28;
|
||||
$[55] = t29;
|
||||
$[56] = t30;
|
||||
$[57] = t31;
|
||||
$[58] = t32;
|
||||
$[59] = t34;
|
||||
$[60] = terminalShortcutElement;
|
||||
$[61] = t35;
|
||||
} else {
|
||||
t35 = $[61];
|
||||
}
|
||||
let t36;
|
||||
if ($[62] !== dimColor || $[63] !== undoShortcut) {
|
||||
t36 = <Box><Text dimColor={dimColor}>{undoShortcut} to undo</Text></Box>;
|
||||
$[62] = dimColor;
|
||||
$[63] = undoShortcut;
|
||||
$[64] = t36;
|
||||
} else {
|
||||
t36 = $[64];
|
||||
}
|
||||
let t37;
|
||||
if ($[65] !== dimColor) {
|
||||
t37 = getPlatform() !== "windows" && <Box><Text dimColor={dimColor}>ctrl + z to suspend</Text></Box>;
|
||||
$[65] = dimColor;
|
||||
$[66] = t37;
|
||||
} else {
|
||||
t37 = $[66];
|
||||
}
|
||||
let t38;
|
||||
if ($[67] !== dimColor || $[68] !== imagePasteShortcut) {
|
||||
t38 = <Box><Text dimColor={dimColor}>{imagePasteShortcut} to paste images</Text></Box>;
|
||||
$[67] = dimColor;
|
||||
$[68] = imagePasteShortcut;
|
||||
$[69] = t38;
|
||||
} else {
|
||||
t38 = $[69];
|
||||
}
|
||||
let t39;
|
||||
if ($[70] !== dimColor || $[71] !== modelPickerShortcut) {
|
||||
t39 = <Box><Text dimColor={dimColor}>{modelPickerShortcut} to switch model</Text></Box>;
|
||||
$[70] = dimColor;
|
||||
$[71] = modelPickerShortcut;
|
||||
$[72] = t39;
|
||||
} else {
|
||||
t39 = $[72];
|
||||
}
|
||||
let t40;
|
||||
if ($[73] !== dimColor || $[74] !== fastModeShortcut) {
|
||||
t40 = isFastModeEnabled() && isFastModeAvailable() && <Box><Text dimColor={dimColor}>{fastModeShortcut} to toggle fast mode</Text></Box>;
|
||||
$[73] = dimColor;
|
||||
$[74] = fastModeShortcut;
|
||||
$[75] = t40;
|
||||
} else {
|
||||
t40 = $[75];
|
||||
}
|
||||
let t41;
|
||||
if ($[76] !== dimColor || $[77] !== stashShortcut) {
|
||||
t41 = <Box><Text dimColor={dimColor}>{stashShortcut} to stash prompt</Text></Box>;
|
||||
$[76] = dimColor;
|
||||
$[77] = stashShortcut;
|
||||
$[78] = t41;
|
||||
} else {
|
||||
t41 = $[78];
|
||||
}
|
||||
let t42;
|
||||
if ($[79] !== dimColor || $[80] !== externalEditorShortcut) {
|
||||
t42 = <Box><Text dimColor={dimColor}>{externalEditorShortcut} to edit in $EDITOR</Text></Box>;
|
||||
$[79] = dimColor;
|
||||
$[80] = externalEditorShortcut;
|
||||
$[81] = t42;
|
||||
} else {
|
||||
t42 = $[81];
|
||||
}
|
||||
let t43;
|
||||
if ($[82] !== dimColor) {
|
||||
t43 = isKeybindingCustomizationEnabled() && <Box><Text dimColor={dimColor}>/keybindings to customize</Text></Box>;
|
||||
$[82] = dimColor;
|
||||
$[83] = t43;
|
||||
} else {
|
||||
t43 = $[83];
|
||||
}
|
||||
let t44;
|
||||
if ($[84] !== t36 || $[85] !== t37 || $[86] !== t38 || $[87] !== t39 || $[88] !== t40 || $[89] !== t41 || $[90] !== t42 || $[91] !== t43) {
|
||||
t44 = <Box flexDirection="column">{t36}{t37}{t38}{t39}{t40}{t41}{t42}{t43}</Box>;
|
||||
$[84] = t36;
|
||||
$[85] = t37;
|
||||
$[86] = t38;
|
||||
$[87] = t39;
|
||||
$[88] = t40;
|
||||
$[89] = t41;
|
||||
$[90] = t42;
|
||||
$[91] = t43;
|
||||
$[92] = t44;
|
||||
} else {
|
||||
t44 = $[92];
|
||||
}
|
||||
let t45;
|
||||
if ($[93] !== gap || $[94] !== paddingX || $[95] !== t27 || $[96] !== t35 || $[97] !== t44) {
|
||||
t45 = <Box paddingX={paddingX} flexDirection="row" gap={gap}>{t27}{t35}{t44}</Box>;
|
||||
$[93] = gap;
|
||||
$[94] = paddingX;
|
||||
$[95] = t27;
|
||||
$[96] = t35;
|
||||
$[97] = t44;
|
||||
$[98] = t45;
|
||||
} else {
|
||||
t45 = $[98];
|
||||
}
|
||||
return t45;
|
||||
dimColor?: boolean
|
||||
fixedWidth?: boolean
|
||||
gap?: number
|
||||
paddingX?: number
|
||||
}
|
||||
|
||||
export function PromptInputHelpMenu(props: Props): React.ReactNode {
|
||||
const { dimColor, fixedWidth, gap, paddingX } = props
|
||||
|
||||
// Get configured shortcuts from keybinding system
|
||||
const transcriptShortcut = formatShortcut(
|
||||
useShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o'),
|
||||
)
|
||||
const todosShortcut = formatShortcut(
|
||||
useShortcutDisplay('app:toggleTodos', 'Global', 'ctrl+t'),
|
||||
)
|
||||
const undoShortcut = formatShortcut(
|
||||
useShortcutDisplay('chat:undo', 'Chat', 'ctrl+_'),
|
||||
)
|
||||
const stashShortcut = formatShortcut(
|
||||
useShortcutDisplay('chat:stash', 'Chat', 'ctrl+s'),
|
||||
)
|
||||
const cycleModeShortcut = formatShortcut(
|
||||
useShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab'),
|
||||
)
|
||||
const modelPickerShortcut = formatShortcut(
|
||||
useShortcutDisplay('chat:modelPicker', 'Chat', 'alt+p'),
|
||||
)
|
||||
const fastModeShortcut = formatShortcut(
|
||||
useShortcutDisplay('chat:fastMode', 'Chat', 'alt+o'),
|
||||
)
|
||||
const externalEditorShortcut = formatShortcut(
|
||||
useShortcutDisplay('chat:externalEditor', 'Chat', 'ctrl+g'),
|
||||
)
|
||||
const terminalShortcut = formatShortcut(
|
||||
useShortcutDisplay('app:toggleTerminal', 'Global', 'meta+j'),
|
||||
)
|
||||
const imagePasteShortcut = formatShortcut(
|
||||
useShortcutDisplay('chat:imagePaste', 'Chat', 'ctrl+v'),
|
||||
)
|
||||
|
||||
// Compute terminal shortcut element outside JSX to satisfy feature() constraint
|
||||
const terminalShortcutElement = feature('TERMINAL_PANEL') ? (
|
||||
getFeatureValue_CACHED_MAY_BE_STALE('tengu_terminal_panel', false) ? (
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>{terminalShortcut} for terminal</Text>
|
||||
</Box>
|
||||
) : null
|
||||
) : null
|
||||
|
||||
return (
|
||||
<Box paddingX={paddingX} flexDirection="row" gap={gap}>
|
||||
<Box flexDirection="column" width={fixedWidth ? 24 : undefined}>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>! for bash mode</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>/ for commands</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>@ for file paths</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>& for background</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>/btw for side question</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexDirection="column" width={fixedWidth ? 35 : undefined}>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>double tap esc to clear input</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>
|
||||
{cycleModeShortcut}{' '}
|
||||
{process.env.USER_TYPE === 'ant'
|
||||
? 'to cycle modes'
|
||||
: 'to auto-accept edits'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>
|
||||
{transcriptShortcut} for verbose output
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>{todosShortcut} to toggle tasks</Text>
|
||||
</Box>
|
||||
{terminalShortcutElement}
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>{getNewlineInstructions()}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>{undoShortcut} to undo</Text>
|
||||
</Box>
|
||||
{getPlatform() !== 'windows' && (
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>ctrl + z to suspend</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>{imagePasteShortcut} to paste images</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>{modelPickerShortcut} to switch model</Text>
|
||||
</Box>
|
||||
{isFastModeEnabled() && isFastModeAvailable() && (
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>
|
||||
{fastModeShortcut} to toggle fast mode
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>{stashShortcut} to stash prompt</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>
|
||||
{externalEditorShortcut} to edit in $EDITOR
|
||||
</Text>
|
||||
</Box>
|
||||
{isKeybindingCustomizationEnabled() && (
|
||||
<Box>
|
||||
<Text dimColor={dimColor}>/keybindings to customize</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from 'src/ink.js';
|
||||
import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from 'src/tools/AgentTool/agentColorManager.js';
|
||||
import type { PromptInputMode } from 'src/types/textInputTypes.js';
|
||||
import { getTeammateColor } from 'src/utils/teammate.js';
|
||||
import type { Theme } from 'src/utils/theme.js';
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from 'src/ink.js'
|
||||
import {
|
||||
AGENT_COLOR_TO_THEME_COLOR,
|
||||
AGENT_COLORS,
|
||||
type AgentColorName,
|
||||
} from 'src/tools/AgentTool/agentColorManager.js'
|
||||
import type { PromptInputMode } from 'src/types/textInputTypes.js'
|
||||
import { getTeammateColor } from 'src/utils/teammate.js'
|
||||
import type { Theme } from 'src/utils/theme.js'
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
|
||||
|
||||
type Props = {
|
||||
mode: PromptInputMode;
|
||||
isLoading: boolean;
|
||||
viewingAgentName?: string;
|
||||
viewingAgentColor?: AgentColorName;
|
||||
};
|
||||
mode: PromptInputMode
|
||||
isLoading: boolean
|
||||
viewingAgentName?: string
|
||||
viewingAgentColor?: AgentColorName
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the theme color key for the teammate's assigned color.
|
||||
@@ -20,73 +24,81 @@ type Props = {
|
||||
*/
|
||||
function getTeammateThemeColor(): keyof Theme | undefined {
|
||||
if (!isAgentSwarmsEnabled()) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
const colorName = getTeammateColor();
|
||||
const colorName = getTeammateColor()
|
||||
if (!colorName) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
if (AGENT_COLORS.includes(colorName as AgentColorName)) {
|
||||
return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName];
|
||||
return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]
|
||||
}
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
type PromptCharProps = {
|
||||
isLoading: boolean;
|
||||
isLoading: boolean
|
||||
// Dead code elimination: parameter named themeColor to avoid "teammate" string in external builds
|
||||
themeColor?: keyof Theme;
|
||||
};
|
||||
themeColor?: keyof Theme
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the prompt character (❯).
|
||||
* Teammate color overrides the default color when set.
|
||||
*/
|
||||
function PromptChar(t0) {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
isLoading,
|
||||
themeColor
|
||||
} = t0;
|
||||
const teammateColor = themeColor;
|
||||
const color = teammateColor ?? (false ? "subtle" : undefined);
|
||||
let t1;
|
||||
if ($[0] !== color || $[1] !== isLoading) {
|
||||
t1 = <Text color={color} dimColor={isLoading}>{figures.pointer} </Text>;
|
||||
$[0] = color;
|
||||
$[1] = isLoading;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
function PromptChar({
|
||||
isLoading,
|
||||
themeColor,
|
||||
}: PromptCharProps): React.ReactNode {
|
||||
// Assign to original name for clarity within the function
|
||||
const teammateColor = themeColor
|
||||
const isAnt = process.env.USER_TYPE === 'ant'
|
||||
const color = teammateColor ?? (isAnt ? 'subtle' : undefined)
|
||||
|
||||
return (
|
||||
<Text color={color} dimColor={isLoading}>
|
||||
{figures.pointer}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
export function PromptInputModeIndicator(t0) {
|
||||
const $ = _c(6);
|
||||
const {
|
||||
mode,
|
||||
isLoading,
|
||||
viewingAgentName,
|
||||
viewingAgentColor
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = getTeammateThemeColor();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const teammateColor = t1;
|
||||
const viewedTeammateThemeColor = viewingAgentColor ? AGENT_COLOR_TO_THEME_COLOR[viewingAgentColor] : undefined;
|
||||
let t2;
|
||||
if ($[1] !== isLoading || $[2] !== mode || $[3] !== viewedTeammateThemeColor || $[4] !== viewingAgentName) {
|
||||
t2 = <Box alignItems="flex-start" alignSelf="flex-start" flexWrap="nowrap" justifyContent="flex-start">{viewingAgentName ? <PromptChar isLoading={isLoading} themeColor={viewedTeammateThemeColor} /> : mode === "bash" ? <Text color="bashBorder" dimColor={isLoading}>! </Text> : <PromptChar isLoading={isLoading} themeColor={isAgentSwarmsEnabled() ? teammateColor : undefined} />}</Box>;
|
||||
$[1] = isLoading;
|
||||
$[2] = mode;
|
||||
$[3] = viewedTeammateThemeColor;
|
||||
$[4] = viewingAgentName;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
return t2;
|
||||
|
||||
export function PromptInputModeIndicator({
|
||||
mode,
|
||||
isLoading,
|
||||
viewingAgentName,
|
||||
viewingAgentColor,
|
||||
}: Props): React.ReactNode {
|
||||
const teammateColor = getTeammateThemeColor()
|
||||
|
||||
// Convert viewed teammate's color to theme color
|
||||
// Falls back to PromptChar's default (subtle for ants, undefined for external)
|
||||
const viewedTeammateThemeColor = viewingAgentColor
|
||||
? AGENT_COLOR_TO_THEME_COLOR[viewingAgentColor]
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Box
|
||||
alignItems="flex-start"
|
||||
alignSelf="flex-start"
|
||||
flexWrap="nowrap"
|
||||
justifyContent="flex-start"
|
||||
>
|
||||
{viewingAgentName ? (
|
||||
// Use teammate's color on the standard prompt character, matching established style
|
||||
<PromptChar
|
||||
isLoading={isLoading}
|
||||
themeColor={viewedTeammateThemeColor}
|
||||
/>
|
||||
) : mode === 'bash' ? (
|
||||
<Text color="bashBorder" dimColor={isLoading}>
|
||||
!
|
||||
</Text>
|
||||
) : (
|
||||
<PromptChar
|
||||
isLoading={isLoading}
|
||||
themeColor={isAgentSwarmsEnabled() ? teammateColor : undefined}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Box } from 'src/ink.js';
|
||||
import { useAppState } from 'src/state/AppState.js';
|
||||
import { STATUS_TAG, SUMMARY_TAG, TASK_NOTIFICATION_TAG } from '../../constants/xml.js';
|
||||
import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js';
|
||||
import { useCommandQueue } from '../../hooks/useCommandQueue.js';
|
||||
import type { QueuedCommand } from '../../types/textInputTypes.js';
|
||||
import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js';
|
||||
import { createUserMessage, EMPTY_LOOKUPS, normalizeMessages } from '../../utils/messages.js';
|
||||
import { jsonParse } from '../../utils/slowOperations.js';
|
||||
import { Message } from '../Message.js';
|
||||
const EMPTY_SET = new Set<string>();
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { Box } from 'src/ink.js'
|
||||
import { useAppState } from 'src/state/AppState.js'
|
||||
import {
|
||||
STATUS_TAG,
|
||||
SUMMARY_TAG,
|
||||
TASK_NOTIFICATION_TAG,
|
||||
} from '../../constants/xml.js'
|
||||
import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js'
|
||||
import { useCommandQueue } from '../../hooks/useCommandQueue.js'
|
||||
import type { QueuedCommand } from '../../types/textInputTypes.js'
|
||||
import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js'
|
||||
import {
|
||||
createUserMessage,
|
||||
EMPTY_LOOKUPS,
|
||||
normalizeMessages,
|
||||
} from '../../utils/messages.js'
|
||||
import { jsonParse } from '../../utils/slowOperations.js'
|
||||
import { Message } from '../Message.js'
|
||||
|
||||
const EMPTY_SET = new Set<string>()
|
||||
|
||||
/**
|
||||
* Check if a command value is an idle notification that should be hidden.
|
||||
@@ -19,15 +28,15 @@ const EMPTY_SET = new Set<string>();
|
||||
*/
|
||||
function isIdleNotification(value: string): boolean {
|
||||
try {
|
||||
const parsed = jsonParse(value);
|
||||
return parsed?.type === 'idle_notification';
|
||||
const parsed = jsonParse(value)
|
||||
return parsed?.type === 'idle_notification'
|
||||
} catch {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Maximum number of task notification lines to show
|
||||
const MAX_VISIBLE_NOTIFICATIONS = 3;
|
||||
const MAX_VISIBLE_NOTIFICATIONS = 3
|
||||
|
||||
/**
|
||||
* Create a synthetic overflow notification message for capped task notifications.
|
||||
@@ -36,7 +45,7 @@ function createOverflowNotificationMessage(count: number): string {
|
||||
return `<${TASK_NOTIFICATION_TAG}>
|
||||
<${SUMMARY_TAG}>+${count} more tasks completed</${SUMMARY_TAG}>
|
||||
<${STATUS_TAG}>completed</${STATUS_TAG}>
|
||||
</${TASK_NOTIFICATION_TAG}>`;
|
||||
</${TASK_NOTIFICATION_TAG}>`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,73 +53,114 @@ function createOverflowNotificationMessage(count: number): string {
|
||||
* Other command types are always shown in full.
|
||||
* Idle notifications are filtered out entirely.
|
||||
*/
|
||||
function processQueuedCommands(queuedCommands: QueuedCommand[]): QueuedCommand[] {
|
||||
function processQueuedCommands(
|
||||
queuedCommands: QueuedCommand[],
|
||||
): QueuedCommand[] {
|
||||
// Filter out idle notifications - they are processed silently
|
||||
const filteredCommands = queuedCommands.filter(cmd => typeof cmd.value !== 'string' || !isIdleNotification(cmd.value));
|
||||
const filteredCommands = queuedCommands.filter(
|
||||
cmd => typeof cmd.value !== 'string' || !isIdleNotification(cmd.value),
|
||||
)
|
||||
|
||||
// Separate task notifications from other commands
|
||||
const taskNotifications = filteredCommands.filter(cmd => cmd.mode === 'task-notification');
|
||||
const otherCommands = filteredCommands.filter(cmd => cmd.mode !== 'task-notification');
|
||||
const taskNotifications = filteredCommands.filter(
|
||||
cmd => cmd.mode === 'task-notification',
|
||||
)
|
||||
const otherCommands = filteredCommands.filter(
|
||||
cmd => cmd.mode !== 'task-notification',
|
||||
)
|
||||
|
||||
// If notifications fit within limit, return all commands as-is
|
||||
if (taskNotifications.length <= MAX_VISIBLE_NOTIFICATIONS) {
|
||||
return [...otherCommands, ...taskNotifications];
|
||||
return [...otherCommands, ...taskNotifications]
|
||||
}
|
||||
|
||||
// Show first (MAX_VISIBLE_NOTIFICATIONS - 1) notifications, then a summary
|
||||
const visibleNotifications = taskNotifications.slice(0, MAX_VISIBLE_NOTIFICATIONS - 1);
|
||||
const overflowCount = taskNotifications.length - (MAX_VISIBLE_NOTIFICATIONS - 1);
|
||||
const visibleNotifications = taskNotifications.slice(
|
||||
0,
|
||||
MAX_VISIBLE_NOTIFICATIONS - 1,
|
||||
)
|
||||
const overflowCount =
|
||||
taskNotifications.length - (MAX_VISIBLE_NOTIFICATIONS - 1)
|
||||
|
||||
// Create synthetic overflow message
|
||||
const overflowCommand: QueuedCommand = {
|
||||
value: createOverflowNotificationMessage(overflowCount),
|
||||
mode: 'task-notification'
|
||||
};
|
||||
return [...otherCommands, ...visibleNotifications, overflowCommand];
|
||||
mode: 'task-notification',
|
||||
}
|
||||
|
||||
return [...otherCommands, ...visibleNotifications, overflowCommand]
|
||||
}
|
||||
|
||||
function PromptInputQueuedCommandsImpl(): React.ReactNode {
|
||||
const queuedCommands = useCommandQueue();
|
||||
const viewingAgent = useAppState(s => !!s.viewingAgentTaskId);
|
||||
const queuedCommands = useCommandQueue()
|
||||
const viewingAgent = useAppState(s => !!s.viewingAgentTaskId)
|
||||
// Brief layout: dim queue items + skip the paddingX (brief messages
|
||||
// already indent themselves). Gate mirrors the brief-spinner/message
|
||||
// check elsewhere — no teammate-view override needed since this
|
||||
// component early-returns when viewing a teammate.
|
||||
const useBriefLayout = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s_0 => s_0.isBriefOnly) : false;
|
||||
const useBriefLayout =
|
||||
feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s => s.isBriefOnly)
|
||||
: false
|
||||
|
||||
// createUserMessage mints a fresh UUID per call; without memoization, streaming
|
||||
// re-renders defeat Message's areMessagePropsEqual (compares uuid) → flicker.
|
||||
const messages = useMemo(() => {
|
||||
if (queuedCommands.length === 0) return null;
|
||||
if (queuedCommands.length === 0) return null
|
||||
// task-notification is shown via useInboxNotification; most isMeta commands
|
||||
// (scheduled tasks, proactive ticks) are system-generated and hidden.
|
||||
// Channel messages are the exception — isMeta but shown so the keyboard
|
||||
// user sees what arrived.
|
||||
const visibleCommands = queuedCommands.filter(isQueuedCommandVisible);
|
||||
if (visibleCommands.length === 0) return null;
|
||||
const processedCommands = processQueuedCommands(visibleCommands);
|
||||
return normalizeMessages(processedCommands.map(cmd => {
|
||||
let content = cmd.value;
|
||||
if (cmd.mode === 'bash' && typeof content === 'string') {
|
||||
content = `<bash-input>${content}</bash-input>`;
|
||||
}
|
||||
// [Image #N] placeholders are inline in the text value (inserted at
|
||||
// paste time), so the queue preview shows them without stub blocks.
|
||||
return createUserMessage({
|
||||
content
|
||||
});
|
||||
}));
|
||||
}, [queuedCommands]);
|
||||
const visibleCommands = queuedCommands.filter(isQueuedCommandVisible)
|
||||
if (visibleCommands.length === 0) return null
|
||||
const processedCommands = processQueuedCommands(visibleCommands)
|
||||
return normalizeMessages(
|
||||
processedCommands.map(cmd => {
|
||||
let content = cmd.value
|
||||
if (cmd.mode === 'bash' && typeof content === 'string') {
|
||||
content = `<bash-input>${content}</bash-input>`
|
||||
}
|
||||
// [Image #N] placeholders are inline in the text value (inserted at
|
||||
// paste time), so the queue preview shows them without stub blocks.
|
||||
return createUserMessage({ content })
|
||||
}),
|
||||
)
|
||||
}, [queuedCommands])
|
||||
|
||||
// Don't show leader's queued commands when viewing any agent's transcript
|
||||
if (viewingAgent || messages === null) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return <Box marginTop={1} flexDirection="column">
|
||||
{messages.map((message, i) => <QueuedMessageProvider key={i} isFirst={i === 0} useBriefLayout={useBriefLayout}>
|
||||
<Message message={message} lookups={EMPTY_LOOKUPS} addMargin={false} tools={[]} commands={[]} verbose={false} inProgressToolUseIDs={EMPTY_SET} progressMessagesForMessage={[]} shouldAnimate={false} shouldShowDot={false} isTranscriptMode={false} isStatic={true} />
|
||||
</QueuedMessageProvider>)}
|
||||
</Box>;
|
||||
|
||||
return (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{messages.map((message, i) => (
|
||||
<QueuedMessageProvider
|
||||
key={i}
|
||||
isFirst={i === 0}
|
||||
useBriefLayout={useBriefLayout}
|
||||
>
|
||||
<Message
|
||||
message={message}
|
||||
lookups={EMPTY_LOOKUPS}
|
||||
addMargin={false}
|
||||
tools={[]}
|
||||
commands={[]}
|
||||
verbose={false}
|
||||
inProgressToolUseIDs={EMPTY_SET}
|
||||
progressMessagesForMessage={[]}
|
||||
shouldAnimate={false}
|
||||
shouldShowDot={false}
|
||||
isTranscriptMode={false}
|
||||
isStatic={true}
|
||||
/>
|
||||
</QueuedMessageProvider>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
export const PromptInputQueuedCommands = React.memo(PromptInputQueuedCommandsImpl);
|
||||
|
||||
export const PromptInputQueuedCommands = React.memo(
|
||||
PromptInputQueuedCommandsImpl,
|
||||
)
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from 'src/ink.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from 'src/ink.js'
|
||||
|
||||
type Props = {
|
||||
hasStash: boolean;
|
||||
};
|
||||
export function PromptInputStashNotice(t0) {
|
||||
const $ = _c(1);
|
||||
const {
|
||||
hasStash
|
||||
} = t0;
|
||||
if (!hasStash) {
|
||||
return null;
|
||||
}
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box paddingLeft={2}><Text dimColor={true}>{figures.pointerSmall} Stashed (auto-restores after submit)</Text></Box>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
return t1;
|
||||
hasStash: boolean
|
||||
}
|
||||
|
||||
export function PromptInputStashNotice({ hasStash }: Props): React.ReactNode {
|
||||
if (!hasStash) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box paddingLeft={2}>
|
||||
<Text dimColor>
|
||||
{figures.pointerSmall} Stashed (auto-restores after submit)
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,63 +1,61 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
|
||||
export function SandboxPromptFooterHint() {
|
||||
const $ = _c(6);
|
||||
const [recentViolationCount, setRecentViolationCount] = useState(0);
|
||||
const timerRef = useRef(null);
|
||||
const detailsShortcut = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
if (!SandboxManager.isSandboxingEnabled()) {
|
||||
return;
|
||||
}
|
||||
const store = SandboxManager.getSandboxViolationStore();
|
||||
let lastCount = store.getTotalCount();
|
||||
const unsubscribe = store.subscribe(() => {
|
||||
const currentCount = store.getTotalCount();
|
||||
const newViolations = currentCount - lastCount;
|
||||
if (newViolations > 0) {
|
||||
setRecentViolationCount(newViolations);
|
||||
lastCount = currentCount;
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
timerRef.current = setTimeout(setRecentViolationCount, 5000, 0);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
import * as React from 'react'
|
||||
import { type ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
|
||||
export function SandboxPromptFooterHint(): ReactNode {
|
||||
const [recentViolationCount, setRecentViolationCount] = useState(0)
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const detailsShortcut = useShortcutDisplay(
|
||||
'app:toggleTranscript',
|
||||
'Global',
|
||||
'ctrl+o',
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!SandboxManager.isSandboxingEnabled()) {
|
||||
return
|
||||
}
|
||||
|
||||
const store = SandboxManager.getSandboxViolationStore()
|
||||
let lastCount = store.getTotalCount()
|
||||
|
||||
const unsubscribe = store.subscribe(() => {
|
||||
const currentCount = store.getTotalCount()
|
||||
const newViolations = currentCount - lastCount
|
||||
|
||||
if (newViolations > 0) {
|
||||
setRecentViolationCount(newViolations)
|
||||
lastCount = currentCount
|
||||
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
clearTimeout(timerRef.current)
|
||||
}
|
||||
};
|
||||
};
|
||||
t1 = [];
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t1 = $[1];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
|
||||
timerRef.current = setTimeout(setRecentViolationCount, 5000, 0)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!SandboxManager.isSandboxingEnabled() || recentViolationCount === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const t2 = recentViolationCount === 1 ? "operation" : "operations";
|
||||
let t3;
|
||||
if ($[2] !== detailsShortcut || $[3] !== recentViolationCount || $[4] !== t2) {
|
||||
t3 = <Box paddingX={0} paddingY={0}><Text color="inactive" wrap="truncate">⧈ Sandbox blocked {recentViolationCount}{" "}{t2} ·{" "}{detailsShortcut} for details · /sandbox to disable</Text></Box>;
|
||||
$[2] = detailsShortcut;
|
||||
$[3] = recentViolationCount;
|
||||
$[4] = t2;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
return t3;
|
||||
|
||||
return (
|
||||
<Box paddingX={0} paddingY={0}>
|
||||
<Text color="inactive" wrap="truncate">
|
||||
⧈ Sandbox blocked {recentViolationCount}{' '}
|
||||
{recentViolationCount === 1 ? 'operation' : 'operations'} ·{' '}
|
||||
{detailsShortcut} for details · /sandbox to disable
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,142 +1,121 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Ansi, Box, Text, useAnimationFrame } from '../../ink.js';
|
||||
import { segmentTextByHighlights, type TextHighlight } from '../../utils/textHighlighting.js';
|
||||
import { ShimmerChar } from '../Spinner/ShimmerChar.js';
|
||||
import * as React from 'react'
|
||||
import { Ansi, Box, Text, useAnimationFrame } from '../../ink.js'
|
||||
import {
|
||||
segmentTextByHighlights,
|
||||
type TextHighlight,
|
||||
} from '../../utils/textHighlighting.js'
|
||||
import { ShimmerChar } from '../Spinner/ShimmerChar.js'
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
highlights: TextHighlight[];
|
||||
};
|
||||
text: string
|
||||
highlights: TextHighlight[]
|
||||
}
|
||||
|
||||
type LinePart = {
|
||||
text: string;
|
||||
highlight: TextHighlight | undefined;
|
||||
start: number;
|
||||
};
|
||||
export function HighlightedInput(t0) {
|
||||
const $ = _c(23);
|
||||
const {
|
||||
text,
|
||||
highlights
|
||||
} = t0;
|
||||
let lines;
|
||||
if ($[0] !== highlights || $[1] !== text) {
|
||||
const segments = segmentTextByHighlights(text, highlights);
|
||||
lines = [[]];
|
||||
let pos = 0;
|
||||
text: string
|
||||
highlight: TextHighlight | undefined
|
||||
start: number
|
||||
}
|
||||
|
||||
export function HighlightedInput({ text, highlights }: Props): React.ReactNode {
|
||||
// The shimmer animation (below) re-renders this component at 20fps while the
|
||||
// ultrathink keyword is present. text/highlights are referentially stable
|
||||
// across animation ticks (parent doesn't re-render), so memoize everything
|
||||
// that derives from them: segmentTextByHighlights alone is ~85µs/call
|
||||
// (tokenize + sort + O(n²) overlap), which adds up fast at 20fps.
|
||||
const { lines, hasShimmer, sweepStart, cycleLength } = React.useMemo(() => {
|
||||
const segments = segmentTextByHighlights(text, highlights)
|
||||
|
||||
// Split segments by newlines into per-line groups. Ink's row-direction Box
|
||||
// indents continuation lines of a multi-line child to that child's X offset.
|
||||
// By splitting at newlines, each line renders as its own row, avoiding the
|
||||
// incorrect indentation when highlighted text is followed by wrapped content.
|
||||
const lines: LinePart[][] = [[]]
|
||||
let pos = 0
|
||||
for (const segment of segments) {
|
||||
const parts = segment.text.split("\n");
|
||||
const parts = segment.text.split('\n')
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i > 0) {
|
||||
lines.push([]);
|
||||
pos = pos + 1;
|
||||
lines.push([])
|
||||
pos += 1
|
||||
}
|
||||
const part = parts[i];
|
||||
const part = parts[i]!
|
||||
if (part.length > 0) {
|
||||
lines[lines.length - 1].push({
|
||||
lines[lines.length - 1]!.push({
|
||||
text: part,
|
||||
highlight: segment.highlight,
|
||||
start: pos
|
||||
});
|
||||
start: pos,
|
||||
})
|
||||
}
|
||||
pos = pos + part.length;
|
||||
pos += part.length
|
||||
}
|
||||
}
|
||||
$[0] = highlights;
|
||||
$[1] = text;
|
||||
$[2] = lines;
|
||||
} else {
|
||||
lines = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== highlights) {
|
||||
t1 = highlights.some(_temp);
|
||||
$[3] = highlights;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
const hasShimmer = t1;
|
||||
let sweepStart = 0;
|
||||
let cycleLength = 1;
|
||||
if (hasShimmer) {
|
||||
let lo = Infinity;
|
||||
let hi = -Infinity;
|
||||
if ($[5] !== hi || $[6] !== highlights || $[7] !== lo) {
|
||||
for (const h_0 of highlights) {
|
||||
if (h_0.shimmerColor) {
|
||||
lo = Math.min(lo, h_0.start);
|
||||
hi = Math.max(hi, h_0.end);
|
||||
|
||||
// Scope the sweep to shimmer-highlighted ranges so cycle time doesn't grow
|
||||
// with input length. Padding creates an offscreen pause between sweeps.
|
||||
const hasShimmer = highlights.some(h => h.shimmerColor)
|
||||
let sweepStart = 0
|
||||
let cycleLength = 1
|
||||
if (hasShimmer) {
|
||||
const padding = 10
|
||||
let lo = Infinity
|
||||
let hi = -Infinity
|
||||
for (const h of highlights) {
|
||||
if (h.shimmerColor) {
|
||||
lo = Math.min(lo, h.start)
|
||||
hi = Math.max(hi, h.end)
|
||||
}
|
||||
}
|
||||
$[5] = hi;
|
||||
$[6] = highlights;
|
||||
$[7] = lo;
|
||||
$[8] = lo;
|
||||
$[9] = hi;
|
||||
} else {
|
||||
lo = $[8] as number;
|
||||
hi = $[9] as number;
|
||||
sweepStart = lo - padding
|
||||
cycleLength = hi - lo + padding * 2
|
||||
}
|
||||
sweepStart = lo - 10;
|
||||
cycleLength = hi - lo + 20;
|
||||
}
|
||||
let t2;
|
||||
if ($[10] !== cycleLength || $[11] !== hasShimmer || $[12] !== lines || $[13] !== sweepStart) {
|
||||
t2 = {
|
||||
lines,
|
||||
hasShimmer,
|
||||
sweepStart,
|
||||
cycleLength
|
||||
};
|
||||
$[10] = cycleLength;
|
||||
$[11] = hasShimmer;
|
||||
$[12] = lines;
|
||||
$[13] = sweepStart;
|
||||
$[14] = t2;
|
||||
} else {
|
||||
t2 = $[14];
|
||||
}
|
||||
const {
|
||||
lines: lines_0,
|
||||
hasShimmer: hasShimmer_0,
|
||||
sweepStart: sweepStart_0,
|
||||
cycleLength: cycleLength_0
|
||||
} = t2;
|
||||
const [ref, time] = useAnimationFrame(hasShimmer_0 ? 50 : null);
|
||||
const glimmerIndex = hasShimmer_0 ? sweepStart_0 + Math.floor(time / 50) % cycleLength_0 : -100;
|
||||
let t3;
|
||||
if ($[15] !== glimmerIndex || $[16] !== lines_0) {
|
||||
let t4;
|
||||
if ($[18] !== glimmerIndex) {
|
||||
t4 = (lineParts, lineIndex) => <Box key={lineIndex}>{lineParts.length === 0 ? <Text> </Text> : lineParts.map((part_0, partIndex) => {
|
||||
if (part_0.highlight?.shimmerColor && part_0.highlight.color) {
|
||||
return <Text key={partIndex}>{part_0.text.split("").map((char, charIndex) => <ShimmerChar key={charIndex} char={char} index={part_0.start + charIndex} glimmerIndex={glimmerIndex} messageColor={part_0.highlight.color} shimmerColor={part_0.highlight.shimmerColor} />)}</Text>;
|
||||
}
|
||||
return <Text key={partIndex} color={part_0.highlight?.color} dimColor={part_0.highlight?.dimColor} inverse={part_0.highlight?.inverse}><Ansi>{part_0.text}</Ansi></Text>;
|
||||
})}</Box>;
|
||||
$[18] = glimmerIndex;
|
||||
$[19] = t4;
|
||||
} else {
|
||||
t4 = $[19];
|
||||
}
|
||||
t3 = lines_0.map(t4);
|
||||
$[15] = glimmerIndex;
|
||||
$[16] = lines_0;
|
||||
$[17] = t3;
|
||||
} else {
|
||||
t3 = $[17];
|
||||
}
|
||||
let t4;
|
||||
if ($[20] !== ref || $[21] !== t3) {
|
||||
t4 = <Box ref={ref} flexDirection="column">{t3}</Box>;
|
||||
$[20] = ref;
|
||||
$[21] = t3;
|
||||
$[22] = t4;
|
||||
} else {
|
||||
t4 = $[22];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
function _temp(h) {
|
||||
return h.shimmerColor;
|
||||
|
||||
return { lines, hasShimmer, sweepStart, cycleLength }
|
||||
}, [text, highlights])
|
||||
|
||||
const [ref, time] = useAnimationFrame(hasShimmer ? 50 : null)
|
||||
const glimmerIndex = hasShimmer
|
||||
? sweepStart + (Math.floor(time / 50) % cycleLength)
|
||||
: -100
|
||||
|
||||
return (
|
||||
<Box ref={ref} flexDirection="column">
|
||||
{lines.map((lineParts, lineIndex) => (
|
||||
<Box key={lineIndex}>
|
||||
{lineParts.length === 0 ? (
|
||||
<Text> </Text>
|
||||
) : (
|
||||
lineParts.map((part, partIndex) => {
|
||||
if (part.highlight?.shimmerColor && part.highlight.color) {
|
||||
return (
|
||||
<Text key={partIndex}>
|
||||
{part.text.split('').map((char, charIndex) => (
|
||||
<ShimmerChar
|
||||
key={charIndex}
|
||||
char={char}
|
||||
index={part.start + charIndex}
|
||||
glimmerIndex={glimmerIndex}
|
||||
messageColor={part.highlight!.color!}
|
||||
shimmerColor={part.highlight!.shimmerColor!}
|
||||
/>
|
||||
))}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Text
|
||||
key={partIndex}
|
||||
color={part.highlight?.color}
|
||||
dimColor={part.highlight?.dimColor}
|
||||
inverse={part.highlight?.inverse}
|
||||
>
|
||||
<Ansi>{part.text}</Ansi>
|
||||
</Text>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,73 +1,32 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import { useSettings } from '../../hooks/useSettings.js';
|
||||
import { Box, Text, useAnimationFrame } from '../../ink.js';
|
||||
import { interpolateColor, toRGBColor } from '../Spinner/utils.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import { useSettings } from '../../hooks/useSettings.js'
|
||||
import { Box, Text, useAnimationFrame } from '../../ink.js'
|
||||
import { interpolateColor, toRGBColor } from '../Spinner/utils.js'
|
||||
|
||||
type Props = {
|
||||
voiceState: 'idle' | 'recording' | 'processing';
|
||||
};
|
||||
voiceState: 'idle' | 'recording' | 'processing'
|
||||
}
|
||||
|
||||
// Processing shimmer colors: dim gray to lighter gray (matches ThinkingShimmerText)
|
||||
const PROCESSING_DIM = {
|
||||
r: 153,
|
||||
g: 153,
|
||||
b: 153
|
||||
};
|
||||
const PROCESSING_BRIGHT = {
|
||||
r: 185,
|
||||
g: 185,
|
||||
b: 185
|
||||
};
|
||||
const PULSE_PERIOD_S = 2; // 2 second period for all pulsing animations
|
||||
const PROCESSING_DIM = { r: 153, g: 153, b: 153 }
|
||||
const PROCESSING_BRIGHT = { r: 185, g: 185, b: 185 }
|
||||
|
||||
export function VoiceIndicator(props) {
|
||||
const $ = _c(2);
|
||||
if (!feature("VOICE_MODE")) {
|
||||
return null;
|
||||
}
|
||||
let t0;
|
||||
if ($[0] !== props) {
|
||||
t0 = <VoiceIndicatorImpl {...props} />;
|
||||
$[0] = props;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
const PULSE_PERIOD_S = 2 // 2 second period for all pulsing animations
|
||||
|
||||
export function VoiceIndicator(props: Props): React.ReactNode {
|
||||
if (!feature('VOICE_MODE')) return null
|
||||
return <VoiceIndicatorImpl {...props} />
|
||||
}
|
||||
function VoiceIndicatorImpl(t0) {
|
||||
const $ = _c(2);
|
||||
const {
|
||||
voiceState
|
||||
} = t0;
|
||||
|
||||
function VoiceIndicatorImpl({ voiceState }: Props): React.ReactNode {
|
||||
switch (voiceState) {
|
||||
case "recording":
|
||||
{
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text dimColor={true}>listening…</Text>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case "processing":
|
||||
{
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <ProcessingShimmer />;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case "idle":
|
||||
{
|
||||
return null;
|
||||
}
|
||||
case 'recording':
|
||||
return <Text dimColor>listening…</Text>
|
||||
case 'processing':
|
||||
return <ProcessingShimmer />
|
||||
case 'idle':
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,62 +34,30 @@ function VoiceIndicatorImpl(t0) {
|
||||
// is too brief for a 1s-period shimmer to register, and a 50ms animation
|
||||
// timer here runs concurrently with auto-repeat spaces arriving every
|
||||
// 30-80ms, compounding re-renders during an already-busy window.
|
||||
export function VoiceWarmupHint() {
|
||||
const $ = _c(1);
|
||||
if (!feature("VOICE_MODE")) {
|
||||
return null;
|
||||
}
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <Text dimColor={true}>keep holding…</Text>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
export function VoiceWarmupHint(): React.ReactNode {
|
||||
if (!feature('VOICE_MODE')) return null
|
||||
return <Text dimColor>keep holding…</Text>
|
||||
}
|
||||
function ProcessingShimmer() {
|
||||
const $ = _c(8);
|
||||
const settings = useSettings();
|
||||
const reducedMotion = settings.prefersReducedMotion ?? false;
|
||||
const [ref, time] = useAnimationFrame(reducedMotion ? null : 50);
|
||||
|
||||
function ProcessingShimmer(): React.ReactNode {
|
||||
const settings = useSettings()
|
||||
const reducedMotion = settings.prefersReducedMotion ?? false
|
||||
const [ref, time] = useAnimationFrame(reducedMotion ? null : 50)
|
||||
|
||||
if (reducedMotion) {
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <Text color="warning">Voice: processing…</Text>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
return <Text color="warning">Voice: processing…</Text>
|
||||
}
|
||||
const elapsedSec = time / 1000;
|
||||
const opacity = (Math.sin(elapsedSec * Math.PI * 2 / PULSE_PERIOD_S) + 1) / 2;
|
||||
let t0;
|
||||
if ($[1] !== opacity) {
|
||||
t0 = toRGBColor(interpolateColor(PROCESSING_DIM, PROCESSING_BRIGHT, opacity));
|
||||
$[1] = opacity;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
const color = t0;
|
||||
let t1;
|
||||
if ($[3] !== color) {
|
||||
t1 = <Text color={color}>Voice: processing…</Text>;
|
||||
$[3] = color;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
let t2;
|
||||
if ($[5] !== ref || $[6] !== t1) {
|
||||
t2 = <Box ref={ref}>{t1}</Box>;
|
||||
$[5] = ref;
|
||||
$[6] = t1;
|
||||
$[7] = t2;
|
||||
} else {
|
||||
t2 = $[7];
|
||||
}
|
||||
return t2;
|
||||
|
||||
const elapsedSec = time / 1000
|
||||
const opacity =
|
||||
(Math.sin((elapsedSec * Math.PI * 2) / PULSE_PERIOD_S) + 1) / 2
|
||||
const color = toRGBColor(
|
||||
interpolateColor(PROCESSING_DIM, PROCESSING_BRIGHT, opacity),
|
||||
)
|
||||
|
||||
return (
|
||||
<Box ref={ref}>
|
||||
<Text color={color}>Voice: processing…</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user