Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions packages/cli/src/config/keyBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,11 +261,14 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.EXPAND_SUGGESTION]: [{ key: 'right' }],
[Command.COLLAPSE_SUGGESTION]: [{ key: 'left' }],

// Thinking expansion
[Command.TOGGLE_THINKING_EXPANDED]: [{ key: 't', meta: true }],
// Thinking expansion (Ctrl+O primary, Alt+T legacy)
[Command.TOGGLE_THINKING_EXPANDED]: [
{ key: 'o', ctrl: true },
{ key: 't', meta: true },
],

// Transcript full-detail screen
[Command.TOGGLE_TRANSCRIPT]: [{ key: 'o', ctrl: true }],
// Transcript overlay — unbound (replaced by inline thinking expansion)
[Command.TOGGLE_TRANSCRIPT]: [],

// Scroll commands
[Command.SCROLL_UP]: [{ key: 'up', shift: true }],
Expand Down
118 changes: 0 additions & 118 deletions packages/cli/src/ui/AppContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4733,124 +4733,6 @@ describe('AppContainer State Management', () => {
});
});

describe('Transcript (Ctrl+O) integration', () => {
// The frozen transcript (TranscriptView) renders its title row as the
// literal "Transcript"; the main view never does, so its presence in the
// rendered frame is a reliable open/closed signal.
const TRANSCRIPT_MARKER = 'Transcript';

const makeKey = (overrides: Partial<Key>): Key =>
({
name: '',
ctrl: false,
meta: false,
shift: false,
paste: false,
sequence: '',
...overrides,
}) as Key;

// The global keypress handler owns Ctrl+O / the transcript close keys; it is
// the registered useKeypress handler whose body references TOGGLE_TRANSCRIPT.
const getGlobalKeypress = () =>
mockedUseKeypress.mock.calls
.map((call) => call[0])
.reverse()
.find(
(handler): handler is (key: Key) => void =>
typeof handler === 'function' &&
handler.toString().includes('TOGGLE_TRANSCRIPT'),
) as ((key: Key) => void) | undefined;

const ctrlO = makeKey({ name: 'o', ctrl: true, sequence: '\x0f' });

const renderApp = () =>
render(
<AppContainer
config={mockConfig}
settings={mockSettings}
version="1.0.0"
initializationResult={mockInitResult}
/>,
);

it('Ctrl+O installs the TranscriptView in the rendered tree', () => {
const { lastFrame } = renderApp();
const handleKeypress = getGlobalKeypress();
expect(handleKeypress).toBeDefined();
// Baseline: the marker also confirms the main view doesn't render it.
expect(lastFrame()).not.toContain(TRANSCRIPT_MARKER);
act(() => handleKeypress!(ctrlO));
expect(lastFrame()).toContain(TRANSCRIPT_MARKER);
});

it.each([
['Esc', makeKey({ name: 'escape', sequence: '\x1b' })],
['q', makeKey({ name: 'q', sequence: 'q' })],
['Ctrl+C', makeKey({ name: 'c', ctrl: true, sequence: '\x03' })],
['Ctrl+D', makeKey({ name: 'd', ctrl: true, sequence: '\x04' })],
// Ctrl+O is the toggle: pressing it again while open also closes.
['Ctrl+O', ctrlO],
])('%s while open removes the TranscriptView', (_label, closeKey) => {
const { lastFrame } = renderApp();
const handleKeypress = getGlobalKeypress()!;
act(() => handleKeypress(ctrlO));
expect(lastFrame()).toContain(TRANSCRIPT_MARKER);
act(() => handleKeypress(closeKey));
expect(lastFrame()).not.toContain(TRANSCRIPT_MARKER);
});

it.each([
['Ctrl+Q', makeKey({ name: 'q', ctrl: true, sequence: '\x11' })],
['Alt+Q', makeKey({ name: 'q', meta: true, sequence: '\x1bq' })],
['Shift+Q', makeKey({ name: 'q', shift: true, sequence: 'Q' })],
])(
'%s does NOT close the transcript (bare-q modifier guard)',
(_l, modQ) => {
const { lastFrame } = renderApp();
const handleKeypress = getGlobalKeypress()!;
act(() => handleKeypress(ctrlO));
expect(lastFrame()).toContain(TRANSCRIPT_MARKER);
act(() => handleKeypress(modQ));
// Only a bare `q` closes; modified variants stay swallowed but open.
expect(lastFrame()).toContain(TRANSCRIPT_MARKER);
},
);

it('swallows arbitrary keys while open and keeps the transcript open', () => {
const { lastFrame } = renderApp();
const handleKeypress = getGlobalKeypress()!;
act(() => handleKeypress(ctrlO));
expect(lastFrame()).toContain(TRANSCRIPT_MARKER);
expect(() =>
act(() => handleKeypress(makeKey({ name: 'x', sequence: 'x' }))),
).not.toThrow();
expect(lastFrame()).toContain(TRANSCRIPT_MARKER);
});

it('auto-closes when a blocking confirmation appears (anti-deadlock)', () => {
// A blocking prompt would be invisible behind the alt-screen transcript;
// the needsBlockingInput effect must close it on the same commit.
mockedUseGeminiStream.mockReturnValue({
streamingState: 'waiting_for_confirmation',
submitQuery: vi.fn(),
initError: null,
pendingHistoryItems: [],
thought: null,
cancelOngoingRequest: vi.fn(),
retryLastPrompt: vi.fn(),
streamingResponseLengthRef: { current: 0 },
isReceivingContent: false,
});
const { lastFrame } = renderApp();
const handleKeypress = getGlobalKeypress()!;
act(() => handleKeypress(ctrlO));
// openTranscript sets the freeze, but the anti-deadlock effect tears it
// back down on the same commit, so it never stays visible.
expect(lastFrame()).not.toContain(TRANSCRIPT_MARKER);
});
});

describe('Model Dialog Integration', () => {
it('should provide isModelDialogOpen in the UIStateContext', () => {
mockedUseModelCommand.mockReturnValue({
Expand Down
Loading
Loading