mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 02:34:15 -05:00
fix: keep footer mounted across slash suggestions (#1943)
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { stripVTControlCharacters as stripAnsi } from 'node:util'
|
||||
import { expect, test } from 'bun:test'
|
||||
import { useEffect } from 'react'
|
||||
import { createRoot, Text } from '../../ink.js'
|
||||
import { KeepMounted } from './KeepMounted.js'
|
||||
|
||||
function createTestStdout(): NodeJS.WriteStream {
|
||||
const stdout = new PassThrough()
|
||||
;(stdout as unknown as { columns: number }).columns = 80
|
||||
return stdout as unknown as NodeJS.WriteStream
|
||||
}
|
||||
|
||||
test('keeps children mounted while visibility changes', async () => {
|
||||
let mounts = 0
|
||||
let unmounts = 0
|
||||
const stdout = createTestStdout() as unknown as PassThrough
|
||||
let output = ''
|
||||
stdout.on('data', chunk => {
|
||||
output += chunk.toString()
|
||||
})
|
||||
|
||||
function Probe() {
|
||||
useEffect(() => {
|
||||
mounts++
|
||||
return () => {
|
||||
unmounts++
|
||||
}
|
||||
}, [])
|
||||
return <Text>persistent child</Text>
|
||||
}
|
||||
|
||||
const root = await createRoot({
|
||||
stdout: stdout as unknown as NodeJS.WriteStream,
|
||||
patchConsole: false,
|
||||
})
|
||||
const render = (hidden: boolean) =>
|
||||
root.render(
|
||||
<KeepMounted hidden={hidden}>
|
||||
<Probe />
|
||||
</KeepMounted>,
|
||||
)
|
||||
|
||||
render(false)
|
||||
await Bun.sleep(10)
|
||||
expect(stripAnsi(output)).toContain('persistent child')
|
||||
|
||||
output = ''
|
||||
render(true)
|
||||
await Bun.sleep(10)
|
||||
const hiddenFrame = stripAnsi(output).replaceAll('\r', '').replaceAll('\n', '')
|
||||
expect(hiddenFrame).toBe('')
|
||||
|
||||
render(false)
|
||||
|
||||
expect(mounts).toBe(1)
|
||||
expect(unmounts).toBe(0)
|
||||
|
||||
root.unmount()
|
||||
expect(unmounts).toBe(1)
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Box } from '../../ink.js'
|
||||
|
||||
export function KeepMounted({
|
||||
hidden,
|
||||
children,
|
||||
}: {
|
||||
hidden: boolean
|
||||
children: ReactNode
|
||||
}): ReactNode {
|
||||
return (
|
||||
<Box height={hidden ? 0 : undefined} overflow="hidden">
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
import { DEFAULT_GLOBAL_CONFIG } from '../../utils/config.js'
|
||||
import {
|
||||
resolveConfiguredFooterStatusLine,
|
||||
resolveFooterStatusLine,
|
||||
SHORTCUTS_HINT_STARTUP_GRACE,
|
||||
shouldSuppressShortcutsHint,
|
||||
@@ -60,6 +61,18 @@ describe('resolveFooterStatusLine', () => {
|
||||
}
|
||||
})
|
||||
|
||||
describe('resolveConfiguredFooterStatusLine', () => {
|
||||
it('keeps a custom statusline configured while transient UI hides its row', () => {
|
||||
expect(resolveConfiguredFooterStatusLine(customSettings)).toBe('custom')
|
||||
expect(
|
||||
resolveFooterStatusLine(customSettings, {
|
||||
...guardsPass,
|
||||
exitMessageShown: true,
|
||||
}),
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldSuppressShortcutsHint', () => {
|
||||
const base = {
|
||||
suppressedByCaller: false,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { BuiltinStatusLine, builtinStatusLineShouldDisplay } from '../BuiltinSta
|
||||
import { useCoordinatorTaskCount } from '../CoordinatorAgentStatus.js';
|
||||
import { getLastAssistantMessageId, StatusLine, statusLineShouldDisplay } from '../StatusLine.js';
|
||||
import { Notifications } from './Notifications.js';
|
||||
import { KeepMounted } from './KeepMounted.js';
|
||||
import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js';
|
||||
import { PromptInputFooterSuggestions, type SuggestionItem } from './PromptInputFooterSuggestions.js';
|
||||
import { PromptInputHelpMenu } from './PromptInputHelpMenu.js';
|
||||
@@ -44,6 +45,15 @@ export function resolveFooterStatusLine(settings: ReadonlySettings, guards: {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveConfiguredFooterStatusLine(settings: ReadonlySettings): 'custom' | 'builtin' | null {
|
||||
return resolveFooterStatusLine(settings, {
|
||||
isPromptMode: true,
|
||||
isShort: false,
|
||||
exitMessageShown: false,
|
||||
isPasting: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The builtin status line ships enabled, so a status line renders for nearly
|
||||
* everyone — treating that as a reason to hide `? for shortcuts` would kill
|
||||
@@ -165,6 +175,7 @@ function PromptInputFooter({
|
||||
exitMessageShown: exitMessage.show,
|
||||
isPasting
|
||||
});
|
||||
const configuredFooterStatusLine = resolveConfiguredFooterStatusLine(settings);
|
||||
// Hide `? for shortcuts` during ctrl-r search, or — for established users
|
||||
// only — when a status line actually renders below (display setting AND
|
||||
// render guards). See shouldSuppressShortcutsHint.
|
||||
@@ -181,25 +192,26 @@ function PromptInputFooter({
|
||||
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>;
|
||||
}
|
||||
if (helpOpen) {
|
||||
return <PromptInputHelpMenu dimColor={true} fixedWidth={true} paddingX={2} />;
|
||||
}
|
||||
const showInlineSuggestions = suggestions.length > 0 && !isFullscreen;
|
||||
const hideRegularFooter = showInlineSuggestions || helpOpen;
|
||||
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}>
|
||||
{footerStatusLine === 'custom' ? <StatusLine messagesRef={messagesRef} lastAssistantMessageId={lastAssistantMessageId} vimMode={vimMode} /> : footerStatusLine === 'builtin' ? <BuiltinStatusLine messagesRef={messagesRef} lastAssistantMessageId={lastAssistantMessageId} /> : null}
|
||||
<KeepMounted hidden={hideRegularFooter}>
|
||||
<Box flexDirection={isNarrow ? 'column' : 'row'} justifyContent={isNarrow ? 'flex-start' : 'space-between'} paddingX={2} gap={isNarrow ? 0 : 1}>
|
||||
<Box flexDirection="column" flexShrink={isNarrow ? 0 : 1}>
|
||||
<KeepMounted hidden={footerStatusLine === null}>
|
||||
{configuredFooterStatusLine === 'custom' ? <StatusLine messagesRef={messagesRef} lastAssistantMessageId={lastAssistantMessageId} vimMode={vimMode} /> : configuredFooterStatusLine === 'builtin' ? <BuiltinStatusLine messagesRef={messagesRef} lastAssistantMessageId={lastAssistantMessageId} /> : null}
|
||||
</KeepMounted>
|
||||
<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}>
|
||||
</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} />}
|
||||
<BridgeStatusIndicator bridgeSelected={bridgeSelected} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</KeepMounted>
|
||||
{showInlineSuggestions ? <Box paddingX={2} paddingY={0}>
|
||||
<PromptInputFooterSuggestions suggestions={suggestions} selectedSuggestion={selectedSuggestion} maxColumnWidth={maxColumnWidth} />
|
||||
</Box> : helpOpen ? <PromptInputHelpMenu dimColor={true} fixedWidth={true} paddingX={2} /> : null}
|
||||
</>;
|
||||
}
|
||||
export default memo(PromptInputFooter);
|
||||
|
||||
Reference in New Issue
Block a user