mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 10:14:19 -05:00
fix(effort): keep effort indicator visible in prompt footer (#1919)
* fix: keep effort indicator visible * fix: preserve footer status for effort fallback * fix: simplify effort footer selection logic * fix: harden effort footer fallback * test: cover brief effort footer suppression * fix: ignore empty footer notifications * test: cover live effort footer updates * fix: handle empty JSX footer notifications
This commit is contained in:
@@ -9,6 +9,11 @@ type IdeStatusIndicatorProps = {
|
||||
ideSelection: IDESelection | undefined;
|
||||
mcpClients?: MCPServerConnection[];
|
||||
};
|
||||
|
||||
export function hasIdeSelection(ideSelection: IDESelection | undefined): boolean {
|
||||
return Boolean(ideSelection?.filePath || (ideSelection?.text && ideSelection.lineCount > 0));
|
||||
}
|
||||
|
||||
export function IdeStatusIndicator(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
@@ -18,7 +23,7 @@ export function IdeStatusIndicator(t0) {
|
||||
const {
|
||||
status: ideStatus
|
||||
} = useIdeConnectionStatus(mcpClients);
|
||||
const shouldShowIdeSelection = ideStatus === "connected" && (ideSelection?.filePath || ideSelection?.text && ideSelection.lineCount > 0);
|
||||
const shouldShowIdeSelection = ideStatus === "connected" && hasIdeSelection(ideSelection);
|
||||
if (ideStatus === null || !shouldShowIdeSelection || !ideSelection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
import { PassThrough } from 'node:stream'
|
||||
|
||||
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
|
||||
import { feature } from 'bun:bundle'
|
||||
import React, { useEffect } from 'react'
|
||||
import { stripVTControlCharacters as stripAnsi } from 'node:util'
|
||||
|
||||
import type { Notification } from '../../context/notifications.js'
|
||||
import type { IDESelection } from '../../hooks/useIdeSelection.js'
|
||||
import { createRoot } from '../../ink.js'
|
||||
import type { MCPServerConnection } from '../../services/mcp/types.js'
|
||||
import {
|
||||
AppStateProvider,
|
||||
getDefaultAppState,
|
||||
useSetAppState,
|
||||
} from '../../state/AppState.js'
|
||||
import {
|
||||
acquireSharedMutationLock,
|
||||
releaseSharedMutationLock,
|
||||
} from '../../test/sharedMutationLock.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import type { EffortValue } from '../../utils/effort.js'
|
||||
import { renderToString } from '../../utils/staticRender.js'
|
||||
|
||||
const actualAutoUpdaterWrapper = await import(
|
||||
`../AutoUpdaterWrapper.js?actual=${Date.now()}-${Math.random()}`
|
||||
)
|
||||
const EFFORT_ENV_KEY = 'CLAUDE_CODE_EFFORT_LEVEL'
|
||||
let savedEffortEnv: string | undefined
|
||||
|
||||
const SYNC_START = '\x1B[?2026h'
|
||||
const SYNC_END = '\x1B[?2026l'
|
||||
|
||||
function extractLastFrame(output: string): string {
|
||||
let lastFrame: string | null = null
|
||||
let cursor = 0
|
||||
while (cursor < output.length) {
|
||||
const start = output.indexOf(SYNC_START, cursor)
|
||||
if (start === -1) break
|
||||
const contentStart = start + SYNC_START.length
|
||||
const end = output.indexOf(SYNC_END, contentStart)
|
||||
if (end === -1) break
|
||||
const frame = output.slice(contentStart, end)
|
||||
if (frame.trim().length > 0) lastFrame = frame
|
||||
cursor = end + SYNC_END.length
|
||||
}
|
||||
return lastFrame ?? output
|
||||
}
|
||||
|
||||
function createTestStreams() {
|
||||
let output = ''
|
||||
const stdout = new PassThrough()
|
||||
const stdin = new PassThrough() as PassThrough & {
|
||||
isTTY: boolean
|
||||
setRawMode: () => void
|
||||
ref: () => void
|
||||
unref: () => void
|
||||
}
|
||||
stdin.isTTY = true
|
||||
stdin.setRawMode = () => {}
|
||||
stdin.ref = () => {}
|
||||
stdin.unref = () => {}
|
||||
;(stdout as unknown as { columns: number }).columns = 120
|
||||
stdout.on('data', chunk => {
|
||||
output += chunk.toString()
|
||||
})
|
||||
return { stdout, stdin, getOutput: () => output }
|
||||
}
|
||||
|
||||
async function waitForFrame(
|
||||
getOutput: () => string,
|
||||
predicate: (frame: string) => boolean,
|
||||
timeoutMs = 3000,
|
||||
): Promise<string> {
|
||||
const startedAt = Date.now()
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const frame = stripAnsi(extractLastFrame(getOutput()))
|
||||
if (predicate(frame)) return frame
|
||||
await Bun.sleep(10)
|
||||
}
|
||||
throw new Error('Timed out waiting for rendered frame')
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await acquireSharedMutationLock(
|
||||
'components/PromptInput/Notifications.effort.test.tsx',
|
||||
)
|
||||
savedEffortEnv = process.env[EFFORT_ENV_KEY]
|
||||
delete process.env[EFFORT_ENV_KEY]
|
||||
mock.module('../AutoUpdaterWrapper.js', () => ({
|
||||
AutoUpdaterWrapper: () => null,
|
||||
}))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
if (savedEffortEnv === undefined) {
|
||||
delete process.env[EFFORT_ENV_KEY]
|
||||
} else {
|
||||
process.env[EFFORT_ENV_KEY] = savedEffortEnv
|
||||
}
|
||||
mock.module('../AutoUpdaterWrapper.js', () => actualAutoUpdaterWrapper)
|
||||
} finally {
|
||||
releaseSharedMutationLock()
|
||||
}
|
||||
})
|
||||
|
||||
async function renderNotifications({
|
||||
effortValue,
|
||||
currentNotification = null,
|
||||
ideSelection = undefined,
|
||||
mcpClients = undefined,
|
||||
isBriefOnly = false,
|
||||
viewingAgentTaskId = undefined,
|
||||
}: {
|
||||
effortValue: EffortValue | undefined
|
||||
currentNotification?: Notification | null
|
||||
ideSelection?: IDESelection
|
||||
mcpClients?: MCPServerConnection[]
|
||||
isBriefOnly?: boolean
|
||||
viewingAgentTaskId?: string
|
||||
}): Promise<string> {
|
||||
const { Notifications } = await import(
|
||||
`./Notifications.js?ts=${Date.now()}-${Math.random()}`
|
||||
)
|
||||
|
||||
return renderToString(
|
||||
<AppStateProvider
|
||||
initialState={{
|
||||
...getDefaultAppState(),
|
||||
mainLoopModelForSession: 'claude-opus-4-8',
|
||||
effortValue,
|
||||
isBriefOnly,
|
||||
viewingAgentTaskId,
|
||||
notifications: {
|
||||
current: currentNotification,
|
||||
queue: [],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Notifications
|
||||
apiKeyStatus="valid"
|
||||
autoUpdaterResult={{ version: null, status: 'success' }}
|
||||
debug={false}
|
||||
isAutoUpdating={false}
|
||||
verbose={false}
|
||||
messages={[] as Message[]}
|
||||
onAutoUpdaterResult={() => {}}
|
||||
onChangeIsUpdating={() => {}}
|
||||
ideSelection={ideSelection}
|
||||
mcpClients={mcpClients}
|
||||
/>
|
||||
</AppStateProvider>,
|
||||
120,
|
||||
)
|
||||
}
|
||||
|
||||
test('renders effort as the stable footer fallback when no notification is active', async () => {
|
||||
const output = await renderNotifications({ effortValue: 'medium' })
|
||||
|
||||
expect(output).toContain('medium · /effort')
|
||||
})
|
||||
|
||||
test('updates the mounted effort footer when app state changes', async () => {
|
||||
const { Notifications } = await import(
|
||||
`./Notifications.js?mounted=${Date.now()}-${Math.random()}`
|
||||
)
|
||||
const { stdout, stdin, getOutput } = createTestStreams()
|
||||
const root = await createRoot({
|
||||
stdout: stdout as unknown as NodeJS.WriteStream,
|
||||
stdin: stdin as unknown as NodeJS.ReadStream,
|
||||
patchConsole: false,
|
||||
})
|
||||
let setAppState: ReturnType<typeof useSetAppState> | undefined
|
||||
|
||||
function AppStateController() {
|
||||
const setState = useSetAppState()
|
||||
useEffect(() => {
|
||||
setAppState = setState
|
||||
}, [setState])
|
||||
return null
|
||||
}
|
||||
|
||||
root.render(
|
||||
<AppStateProvider
|
||||
initialState={{
|
||||
...getDefaultAppState(),
|
||||
mainLoopModelForSession: 'claude-opus-4-8',
|
||||
effortValue: 'high',
|
||||
}}
|
||||
>
|
||||
<Notifications
|
||||
apiKeyStatus="valid"
|
||||
autoUpdaterResult={{ version: null, status: 'success' }}
|
||||
debug={false}
|
||||
isAutoUpdating={false}
|
||||
verbose={false}
|
||||
messages={[] as Message[]}
|
||||
onAutoUpdaterResult={() => {}}
|
||||
onChangeIsUpdating={() => {}}
|
||||
ideSelection={undefined}
|
||||
mcpClients={undefined}
|
||||
/>
|
||||
<AppStateController />
|
||||
</AppStateProvider>,
|
||||
)
|
||||
|
||||
try {
|
||||
const initialFrame = await waitForFrame(getOutput, frame =>
|
||||
frame.includes('high · /effort'),
|
||||
)
|
||||
expect(initialFrame).toContain('high · /effort')
|
||||
|
||||
setAppState!(state => ({ ...state, effortValue: 'low' }))
|
||||
|
||||
const updatedFrame = await waitForFrame(getOutput, frame =>
|
||||
frame.includes('low · /effort'),
|
||||
)
|
||||
expect(updatedFrame).toContain('low · /effort')
|
||||
expect(updatedFrame).not.toContain('high · /effort')
|
||||
} finally {
|
||||
root.unmount()
|
||||
stdin.end()
|
||||
stdout.end()
|
||||
await Bun.sleep(0)
|
||||
}
|
||||
})
|
||||
|
||||
test('lets transient notifications temporarily occupy the footer slot', async () => {
|
||||
const output = await renderNotifications({
|
||||
effortValue: 'medium',
|
||||
currentNotification: {
|
||||
key: 'other',
|
||||
text: 'Other notice',
|
||||
priority: 'high',
|
||||
},
|
||||
})
|
||||
|
||||
expect(output).toContain('Other notice')
|
||||
expect(output).not.toContain('medium · /effort')
|
||||
})
|
||||
|
||||
test('ignores empty text notifications when rendering the effort footer fallback', async () => {
|
||||
const output = await renderNotifications({
|
||||
effortValue: 'medium',
|
||||
currentNotification: {
|
||||
key: 'empty',
|
||||
text: '',
|
||||
priority: 'low',
|
||||
},
|
||||
})
|
||||
|
||||
expect(output).toContain('medium · /effort')
|
||||
})
|
||||
|
||||
test('ignores empty JSX notifications when rendering the effort footer fallback', async () => {
|
||||
const output = await renderNotifications({
|
||||
effortValue: 'medium',
|
||||
currentNotification: {
|
||||
key: 'empty-jsx',
|
||||
jsx: null,
|
||||
priority: 'low',
|
||||
},
|
||||
})
|
||||
|
||||
expect(output).toContain('medium · /effort')
|
||||
})
|
||||
|
||||
test('preserves IDE selection status before the effort fallback', async () => {
|
||||
const output = await renderNotifications({
|
||||
effortValue: 'medium',
|
||||
ideSelection: {
|
||||
lineCount: 0,
|
||||
filePath: '/tmp/example.ts',
|
||||
},
|
||||
mcpClients: [
|
||||
{
|
||||
name: 'ide',
|
||||
type: 'connected',
|
||||
capabilities: {},
|
||||
config: {
|
||||
type: 'sse-ide',
|
||||
url: 'http://localhost:1234',
|
||||
ideName: 'VS Code',
|
||||
scope: 'local',
|
||||
},
|
||||
client: {},
|
||||
cleanup: async () => {},
|
||||
} as unknown as MCPServerConnection,
|
||||
],
|
||||
})
|
||||
|
||||
expect(output).toContain('In example.ts')
|
||||
expect(output).not.toContain('medium · /effort')
|
||||
})
|
||||
|
||||
if (feature('KAIROS') || feature('KAIROS_BRIEF')) {
|
||||
test('respects brief footer ownership and teammate view', async () => {
|
||||
const briefOutput = await renderNotifications({
|
||||
effortValue: 'medium',
|
||||
isBriefOnly: true,
|
||||
})
|
||||
const teammateOutput = await renderNotifications({
|
||||
effortValue: 'medium',
|
||||
isBriefOnly: true,
|
||||
viewingAgentTaskId: 'task-123',
|
||||
})
|
||||
|
||||
expect(briefOutput).not.toContain('medium · /effort')
|
||||
expect(teammateOutput).toContain('medium · /effort')
|
||||
})
|
||||
}
|
||||
@@ -27,7 +27,8 @@ 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 { getEffortNotificationText } from '../EffortIndicator.js';
|
||||
import { hasIdeSelection, IdeStatusIndicator } from '../IdeStatusIndicator.js';
|
||||
import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js';
|
||||
import { SentryErrorBoundary } from '../SentryErrorBoundary.js';
|
||||
import { TokenWarning } from '../TokenWarning.js';
|
||||
@@ -53,7 +54,7 @@ type Props = {
|
||||
isNarrow?: boolean;
|
||||
};
|
||||
export function Notifications(t0) {
|
||||
const $ = _c(34);
|
||||
const $ = _c(35);
|
||||
const {
|
||||
apiKeyStatus,
|
||||
autoUpdaterResult,
|
||||
@@ -124,7 +125,7 @@ export function Notifications(t0) {
|
||||
t6 = $[7];
|
||||
}
|
||||
useEffect(t5, t6);
|
||||
const shouldShowIdeSelection = ideStatus === "connected" && (ideSelection?.filePath || ideSelection?.text && ideSelection.lineCount > 0);
|
||||
const shouldShowIdeSelection = ideStatus === "connected" && hasIdeSelection(ideSelection);
|
||||
const shouldShowAutoUpdater = !shouldShowIdeSelection || isAutoUpdating || autoUpdaterResult?.status !== "success";
|
||||
const isInOverageMode = claudeAiLimits.isUsingOverage;
|
||||
let t7;
|
||||
@@ -175,8 +176,8 @@ export function Notifications(t0) {
|
||||
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} />;
|
||||
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] !== shouldShowIdeSelection || $[30] !== 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} shouldShowIdeSelection={shouldShowIdeSelection} autoUpdaterResult={autoUpdaterResult} isAutoUpdating={isAutoUpdating} isShowingCompactMessage={isShowingCompactMessage} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={onChangeIsUpdating} />;
|
||||
$[15] = apiKeyStatus;
|
||||
$[16] = autoUpdaterResult;
|
||||
$[17] = debug;
|
||||
@@ -191,19 +192,20 @@ export function Notifications(t0) {
|
||||
$[26] = shouldShowAutoUpdater;
|
||||
$[27] = t12;
|
||||
$[28] = tokenUsage;
|
||||
$[29] = verbose;
|
||||
$[30] = t13;
|
||||
$[29] = shouldShowIdeSelection;
|
||||
$[30] = verbose;
|
||||
$[31] = t13;
|
||||
} else {
|
||||
t13 = $[30];
|
||||
t13 = $[31];
|
||||
}
|
||||
let t14;
|
||||
if ($[31] !== t11 || $[32] !== t13) {
|
||||
if ($[32] !== t11 || $[33] !== t13) {
|
||||
t14 = <SentryErrorBoundary><Box flexDirection="column" alignItems={t11} flexShrink={0} overflowX="hidden">{t13}</Box></SentryErrorBoundary>;
|
||||
$[31] = t11;
|
||||
$[32] = t13;
|
||||
$[33] = t14;
|
||||
$[32] = t11;
|
||||
$[33] = t13;
|
||||
$[34] = t14;
|
||||
} else {
|
||||
t14 = $[33];
|
||||
t14 = $[34];
|
||||
}
|
||||
return t14;
|
||||
}
|
||||
@@ -225,6 +227,7 @@ function NotificationContent({
|
||||
tokenUsage,
|
||||
mainLoopModel,
|
||||
shouldShowAutoUpdater,
|
||||
shouldShowIdeSelection,
|
||||
autoUpdaterResult,
|
||||
isAutoUpdating,
|
||||
isShowingCompactMessage,
|
||||
@@ -245,6 +248,7 @@ function NotificationContent({
|
||||
tokenUsage: number;
|
||||
mainLoopModel: string;
|
||||
shouldShowAutoUpdater: boolean;
|
||||
shouldShowIdeSelection: boolean;
|
||||
autoUpdaterResult: AutoUpdaterResult | null;
|
||||
isAutoUpdating: boolean;
|
||||
isShowingCompactMessage: boolean;
|
||||
@@ -277,6 +281,31 @@ function NotificationContent({
|
||||
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 viewingAgentTaskId = feature('KAIROS') || feature('KAIROS_BRIEF') ?
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
||||
useAppState(s_2 => s_2.viewingAgentTaskId) : undefined;
|
||||
const briefOwnsGap = isBriefOnly && !viewingAgentTaskId;
|
||||
const shouldShowEffortFallback = !briefOwnsGap && !shouldShowIdeSelection;
|
||||
const effortValue = useAppState(s_3 => s_3.effortValue);
|
||||
const effortNotificationText = shouldShowEffortFallback ? getEffortNotificationText(effortValue, mainLoopModel) : undefined;
|
||||
let notificationNode: ReactNode = null;
|
||||
if (notifications.current) {
|
||||
if ('jsx' in notifications.current) {
|
||||
const notificationJsx = notifications.current.jsx;
|
||||
if (notificationJsx !== null && notificationJsx !== undefined && notificationJsx !== '' && typeof notificationJsx !== 'boolean') {
|
||||
notificationNode = <Text wrap="truncate" key={notifications.current.key}>
|
||||
{notificationJsx}
|
||||
</Text>;
|
||||
}
|
||||
} else if (notifications.current.text) {
|
||||
notificationNode = <Text color={notifications.current.color} dimColor={!notifications.current.color} wrap="truncate">
|
||||
{notifications.current.text}
|
||||
</Text>;
|
||||
}
|
||||
}
|
||||
const effortFallbackNode = effortNotificationText ? <Text dimColor wrap="truncate" key="effort-fallback">
|
||||
{effortNotificationText}
|
||||
</Text> : null;
|
||||
|
||||
// When voice is actively recording or processing, replace all
|
||||
// notifications with just the voice indicator.
|
||||
@@ -285,11 +314,7 @@ function NotificationContent({
|
||||
}
|
||||
return <>
|
||||
<IdeStatusIndicator ideSelection={ideSelection} mcpClients={mcpClients} />
|
||||
{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">
|
||||
{notifications.current.text}
|
||||
</Text>)}
|
||||
{notificationNode ?? effortFallbackNode}
|
||||
{isInOverageMode && !isTeamOrEnterprise && <Box>
|
||||
<Text dimColor wrap="truncate">
|
||||
Now using extra usage
|
||||
|
||||
@@ -99,7 +99,6 @@ import { AutoModeOptInDialog } from '../AutoModeOptInDialog.js';
|
||||
import { BridgeDialog } from '../BridgeDialog.js';
|
||||
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
|
||||
import { getVisibleAgentTasks, useCoordinatorTaskCount } from '../CoordinatorAgentStatus.js';
|
||||
import { getEffortNotificationText } from '../EffortIndicator.js';
|
||||
import { getFastIconString } from '../FastIcon.js';
|
||||
import { GlobalSearchDialog } from '../GlobalSearchDialog.js';
|
||||
import { HistorySearchDialog } from '../HistorySearchDialog.js';
|
||||
@@ -336,7 +335,6 @@ function PromptInput({
|
||||
const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession);
|
||||
const thinkingEnabled = useAppState(s => s.thinkingEnabled);
|
||||
const isFastMode = useAppState(s => isFastModeEnabled() ? s.fastMode : false);
|
||||
const effortValue = useAppState(s => s.effortValue);
|
||||
const viewedTeammate = getViewedTeammateTask(store.getState());
|
||||
const viewingAgentName = viewedTeammate?.identity.agentName;
|
||||
// identity.color is typed as `string | undefined` (not AgentColorName) because
|
||||
@@ -2013,22 +2011,6 @@ function PromptInput({
|
||||
const showFastIcon = isFastModeEnabled() ? isFastMode && (isFastModeAvailable() || fastModeCooldown) : false;
|
||||
const showFastIconHint = useShowFastIconHint(showFastIcon ?? false);
|
||||
|
||||
// Show effort notification on startup and when effort changes.
|
||||
// Suppressed in brief/assistant mode — the value reflects the local
|
||||
// client's effort, not the connected agent's.
|
||||
const effortNotificationText = briefOwnsGap ? undefined : getEffortNotificationText(effortValue, mainLoopModel);
|
||||
useEffect(() => {
|
||||
if (!effortNotificationText) {
|
||||
removeNotification('effort-level');
|
||||
return;
|
||||
}
|
||||
addNotification({
|
||||
key: 'effort-level',
|
||||
text: effortNotificationText,
|
||||
priority: 'high',
|
||||
timeoutMs: 12_000
|
||||
});
|
||||
}, [effortNotificationText, addNotification, removeNotification]);
|
||||
useBuddyNotification();
|
||||
const companionSpeaking = isBuddyEnabled() ?
|
||||
useAppState(s => s.companionReaction !== undefined) : false;
|
||||
|
||||
Reference in New Issue
Block a user