Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -778,9 +778,9 @@ const SETTINGS_SCHEMA = {
label: 'Enable Follow-up Suggestions',
category: 'UI',
requiresRestart: false,
default: false,
default: true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The description on line 783 still says "Enter to accept and submit", but this PR changed Enter to only fill the buffer (matching Tab/Right Arrow). Since enableFollowupSuggestions now defaults to true, every user will see this misleading description in settings.

Update the description in both settingsSchema.ts and settings.schema.json:

Suggested change
default: true,
default: true,
description:
'Show context-aware follow-up suggestions after task completion. Press Tab, Right Arrow, or Enter to accept into the input buffer.',

— qwen3.7-max via Qwen Code /review

description:
'Show context-aware follow-up suggestions after task completion. Press Tab or Right Arrow to accept, Enter to accept and submit.',
'Show context-aware follow-up suggestions after task completion. Press Tab, Right Arrow, or Enter to accept into the input buffer.',
showInDialog: true,
},
enableCacheSharing: {
Expand Down
25 changes: 17 additions & 8 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1470,11 +1470,19 @@ export const AppContainer = (props: AppContainerProps) => {
const speculationRef = useRef<SpeculationState>(IDLE_SPECULATION);
const suggestionAbortRef = useRef<AbortController | null>(null);

// Dismiss callback — clears suggestion + aborts in-flight generation/speculation
const dismissPromptSuggestion = useCallback(() => {
setPromptSuggestion(null);
// Aborts in-flight suggestion generation/speculation only. It deliberately
// does NOT clear `promptSuggestion`, so the placeholder can restore the
// suggestion when the buffer becomes empty again (user types then deletes).
// Named "abort" (not "dismiss") precisely because the suggestion text
// survives — see #5145 review.
const abortPromptSuggestion = useCallback(() => {
suggestionAbortRef.current?.abort();
suggestionAbortRef.current = null;
// Also abort the speculation so it doesn't continue running after abort.
if (speculationRef.current.status !== 'idle') {
abortSpeculation(speculationRef.current).catch(() => {});
speculationRef.current = IDLE_SPECULATION;
}
}, []);

// Auto-accept indicator — disabled on agent tabs (agents handle their own)
Expand Down Expand Up @@ -2207,9 +2215,10 @@ export const AppContainer = (props: AppContainerProps) => {
settingInputRequests,
]);

// Abort speculation when promptSuggestion is cleared (new turn, feature toggle, or
// user-initiated dismiss via typing/paste). InputPrompt calls onPromptSuggestionDismiss
// on user input, which clears promptSuggestion, triggering this effect to abort speculation.
// Abort speculation when promptSuggestion is cleared (new turn or feature toggle).
// promptSuggestion is only cleared when the model responds or the feature is disabled;
// user typing/paste no longer dismisses it — the AbortController in InputPrompt handles
// that path, so this effect only fires on state changes from non-user-input sources.
useEffect(() => {
if (!promptSuggestion && speculationRef.current.status !== 'idle') {
abortSpeculation(speculationRef.current).catch(() => {});
Expand Down Expand Up @@ -3385,7 +3394,7 @@ export const AppContainer = (props: AppContainerProps) => {
setSessionName,
// Prompt suggestion
promptSuggestion,
dismissPromptSuggestion,
abortPromptSuggestion,
// Rewind selector
isRewindSelectorOpen,
rewindEscPending,
Expand Down Expand Up @@ -3518,7 +3527,7 @@ export const AppContainer = (props: AppContainerProps) => {
setSessionName,
// Prompt suggestion
promptSuggestion,
dismissPromptSuggestion,
abortPromptSuggestion,
// Rewind selector
isRewindSelectorOpen,
rewindEscPending,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/ui/components/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export const Composer = () => {
: ' ' + t('Type your message or @path/to/file')
}
promptSuggestion={uiState.promptSuggestion}
onPromptSuggestionDismiss={uiState.dismissPromptSuggestion}
onPromptSuggestionDismiss={uiState.abortPromptSuggestion}
/>
)}

Expand Down
137 changes: 131 additions & 6 deletions packages/cli/src/ui/components/InputPrompt.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,10 @@ describe('InputPrompt', () => {
// generous buffer (renderWithProviders cold start can be 100-200ms).
const SUGGESTION_VISIBLE_WAIT_MS = 700;

it('accepts and submits the prompt suggestion on Enter when the buffer is empty', async () => {
// Regression: Enter on suggestion should fill buffer, NOT submit — matches
// Tab/Right-arrow behavior and Claude Code's design. This prevents accidental
// execution of destructive slash commands (/clear, /quit).
it('fills buffer on Enter when suggestion is available (does not submit)', async () => {
vi.useFakeTimers();
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} promptSuggestion="commit this" />,
Expand All @@ -392,11 +395,9 @@ describe('InputPrompt', () => {
});
await flush();

expect(props.onSubmit).toHaveBeenCalledWith('commit this');
// Enter path must NOT call buffer.insert — it passes text directly to
// handleSubmitAndClear. Calling insert would re-fill the buffer after
// it was already cleared (the microtask race bug).
expect(mockBuffer.insert).not.toHaveBeenCalled();
// Enter on suggestion should fill buffer, NOT submit
expect(props.onSubmit).not.toHaveBeenCalled();
expect(mockBuffer.insert).toHaveBeenCalledWith('commit this');
} finally {
vi.useRealTimers();
unmount();
Expand Down Expand Up @@ -452,6 +453,86 @@ describe('InputPrompt', () => {
unmount();
}
});

// Regression for #5145: the `promptSuggestion` prop fallback path must work
// when `followup.state.suggestion` is null (e.g. after user typed and
// deleted — the followup controller was dismissed but the placeholder
// text is still available via the prop).
//
// These tests deliberately advance LESS than SUGGESTION_DELAY_MS (300ms),
// so the followup controller's show-timer never fires and
// `followup.state.suggestion` stays null — exercising the prop fallback
// branch rather than the normal visible-suggestion path. (Earlier versions
// waited 700ms here, which silently tested the normal flow instead.)
describe('promptSuggestion prop fallback (when followup.state.suggestion is null)', () => {
// Comfortably under the 300ms SUGGESTION_DELAY_MS so the controller's
// state.suggestion remains null.
const BEFORE_SUGGESTION_VISIBLE_MS = 100;

it('accepts promptSuggestion via Tab when followup.state.suggestion is null', async () => {
vi.useFakeTimers();
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} promptSuggestion="commit this" />,
);
try {
await advanceTimers(BEFORE_SUGGESTION_VISIBLE_MS);

act(() => {
stdin.write('\t');
});
await flush();

// Tab should insert the suggestion text into the buffer
expect(mockBuffer.insert).toHaveBeenCalledWith('commit this');
} finally {
vi.useRealTimers();
unmount();
}
});

it('accepts promptSuggestion via Right arrow when followup.state.suggestion is null', async () => {
vi.useFakeTimers();
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} promptSuggestion="commit this" />,
);
try {
await advanceTimers(BEFORE_SUGGESTION_VISIBLE_MS);

act(() => {
stdin.write('\x1b[C'); // right arrow
});
await flush();

// Right arrow should insert the suggestion text into the buffer
expect(mockBuffer.insert).toHaveBeenCalledWith('commit this');
} finally {
vi.useRealTimers();
unmount();
}
});

it('fills buffer on Enter (does not submit) when followup.state.suggestion is null', async () => {
vi.useFakeTimers();
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} promptSuggestion="commit this" />,
);
try {
await advanceTimers(BEFORE_SUGGESTION_VISIBLE_MS);

act(() => {
stdin.write('\r');
});
await flush();

// Enter on suggestion should fill buffer, NOT submit (matches Tab/Right-arrow behavior)
expect(props.onSubmit).not.toHaveBeenCalled();
expect(mockBuffer.insert).toHaveBeenCalledWith('commit this');
} finally {
vi.useRealTimers();
unmount();
}
});
});
});

// Regression for #4171: `onTabConsumerChange` (consumed by AppContainer
Expand All @@ -478,6 +559,50 @@ describe('InputPrompt', () => {
unmount();
});

// Regression for #5145: `hasTabConsumer` must be true when ONLY
// `promptSuggestion` prop is set (followup.state.suggestion is null),
// so Windows Tab approval-mode cycling is blocked even before the
// followup controller's debounce fires.
it('reports true immediately when promptSuggestion prop is set (no followup debounce needed)', async () => {
const onTabConsumerChange = vi.fn();
const { unmount } = renderWithProviders(
<InputPrompt
{...props}
promptSuggestion="commit this"
onTabConsumerChange={onTabConsumerChange}
/>,
);
// Must report true immediately — no need to wait for followup debounce
// because `hasTabConsumer` includes `Boolean(promptSuggestion)`.
expect(onTabConsumerChange).toHaveBeenCalledWith(true);
unmount();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The core behavioral change of this PR — replacing suggestionDismissed (sticky until new suggestion) with buffer.text.length === 0 (reactive to current buffer) — has two untested transitions:

  1. Type-then-delete round trip: after typing then deleting back to empty, hasTabConsumer should restore to true. The old suggestionDismissed would have kept it false. No test covers this divergence.

  2. Type-to-non-empty: hasTabConsumer should drop to false when the user types while promptSuggestion is set. This is the exact transition the old setSuggestionDismissed(true) handled.

Consider adding a test that renders with promptSuggestion, types a character (asserts onTabConsumerChange(false)), backspaces to empty (asserts onTabConsumerChange(true) again). This pins the behavioral contract of the new buffer.text.length === 0 gate.

— qwen3.7-max via Qwen Code /review

});

// Regression for #5145 (wenshao review): `hasTabConsumer` is now gated on
// `buffer.text.length === 0` instead of the old sticky `suggestionDismissed`
// flag, so it reacts to the *current* buffer. With a promptSuggestion present
// but a NON-empty buffer (the user is mid-typing), Tab must NOT be consumed —
// Windows approval-mode cycling stays enabled. The old `Boolean(promptSuggestion)`
// gate wrongly reported true here. The complementary empty-buffer → true
// direction (i.e. restored after deleting back to empty) is pinned by
// "reports true immediately when promptSuggestion prop is set" above.
it('reports false when a promptSuggestion is set but the buffer is non-empty', async () => {
mockBuffer.text = 'commit';
const onTabConsumerChange = vi.fn();
const { unmount } = renderWithProviders(
<InputPrompt
{...props}
promptSuggestion="commit this"
onTabConsumerChange={onTabConsumerChange}
/>,
);
await wait();

expect(onTabConsumerChange).toHaveBeenCalledWith(false);
expect(onTabConsumerChange).not.toHaveBeenCalledWith(true);
unmount();
});

it('reports true while mid-input ghost text offers an accept', async () => {
mockCommandCompletion.midInputGhostText = {
text: 'ile.txt',
Expand Down
69 changes: 42 additions & 27 deletions packages/cli/src/ui/components/InputPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,16 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
setAgentTabBarFocused,
]);

// Single source of truth for "is there a suggestion the user can accept right
// now": the live followup suggestion if visible, otherwise the persisted
// `promptSuggestion` prop (type-then-delete / pre-show-delay). Tab/Right/Enter

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] handleSubmitAndClear (line 395) calls followup.dismiss() but does NOT call onPromptSuggestionDismiss?.(). Since this PR changed abortPromptSuggestion (formerly dismissPromptSuggestion) to preserve promptSuggestion state for type-then-delete restore, the submit path now leaks stale suggestion text.

After running a synchronous slash command (e.g., /clear, /help) that doesn't trigger the streaming transition effect in AppContainer, promptSuggestion is never cleared, and the placeholder shows the old suggestion. Compare with the paste handler at line 659 and the typing handler at line 1356, which both correctly call onPromptSuggestionDismiss?.() after followup.dismiss().

Suggested change
// `promptSuggestion` prop (type-then-delete / pre-show-delay). Tab/Right/Enter
// Single source of truth for "is there a suggestion the user can accept right
// now": the live followup suggestion if visible, otherwise the persisted
// `promptSuggestion` prop (type-then-delete / pre-show-delay). Tab/Right/Enter
// accept, the typing-dismiss guards, and the placeholder all derive from this
// so they can never drift apart.
const availableSuggestion: string | null =
followup.state.isVisible || promptSuggestion
? (followup.state.suggestion ?? promptSuggestion ?? null)
: null;

Suggested fix: Add onPromptSuggestionDismiss?.() after followup.dismiss() in handleSubmitAndClear:

followup.dismiss();
onPromptSuggestionDismiss?.(); // abort generation + speculation + clear state

— qwen3.7-max via Qwen Code /review

// accept, the typing-dismiss guards, and the placeholder all derive from this
// so they can never drift apart.
const availableSuggestion: string | null =
followup.state.isVisible || promptSuggestion
? (followup.state.suggestion ?? promptSuggestion ?? null)
: null;

const handleInput = useCallback(
(key: Key): boolean => {
// When the Arena tab bar or background pill has focus, block
Expand Down Expand Up @@ -646,7 +656,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({

if (key.paste) {
// Dismiss follow-up suggestion when user starts typing/pasting
if (buffer.text.length === 0 && followup.state.isVisible) {
if (buffer.text.length === 0 && availableSuggestion) {
followup.dismiss();
onPromptSuggestionDismiss?.();
}
Expand Down Expand Up @@ -987,10 +997,12 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
!showCompletionSuggestions &&
!reverseSearchActive &&
!commandSearchActive &&
followup.state.isVisible &&
followup.state.suggestion
availableSuggestion
) {
followup.accept('tab');
// Use the normal accept path. When the followup controller has no live
// suggestion (e.g. after type-then-delete), `fallbackText` carries the
// still-available placeholder text so telemetry is logged either way.
followup.accept('tab', { fallbackText: promptSuggestion ?? undefined });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] After accepting a suggestion via Tab, Right Arrow, or Enter, promptSuggestion is never cleared. followup.accept(...) resets the controller state to INITIAL, but the promptSuggestion prop from AppContainer persists. When the user subsequently clears the buffer without submitting (e.g., Ctrl+U, Ctrl+C), availableSuggestion evaluates back to the old promptSuggestion value and the just-accepted suggestion reappears as a ghost placeholder.

Repro: Model suggests "/clear" → Tab to accept (buffer now shows "/clear") → Ctrl+U to clear buffer → "/clear" reappears as placeholder.

This applies to all three accept handlers (Tab at line 1005, Right-arrow at line 1017, Enter at line 1261).

Suggested fix: Add onPromptSuggestionDismiss?.() after each followup.accept(...) call:

followup.accept('tab', { fallbackText: promptSuggestion ?? undefined });
onPromptSuggestionDismiss?.();
return true;

— qwen3.7-max via Qwen Code /review

return true;
}

Expand All @@ -1000,10 +1012,11 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
!key.ctrl &&
!key.meta &&
buffer.text.length === 0 &&
followup.state.isVisible &&
followup.state.suggestion
availableSuggestion
) {
followup.accept('right');
followup.accept('right', {
fallbackText: promptSuggestion ?? undefined,
});
return true;
}

Expand Down Expand Up @@ -1239,19 +1252,15 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
}

if (keyMatchers[Command.SUBMIT](key)) {
// Accept and submit prompt suggestion on Enter when input is truly empty
if (
buffer.text.length === 0 &&
followup.state.isVisible &&
followup.state.suggestion
) {
const text = followup.state.suggestion;
// Skip onAccept (buffer.insert) — we pass the text directly to
// handleSubmitAndClear which clears the buffer synchronously.
// Without skipOnAccept the microtask in accept() would re-insert
// the suggestion into the buffer after it was already cleared.
followup.accept('enter', { skipOnAccept: true });
handleSubmitAndClear(text);
// When buffer is empty and a suggestion is available, Enter fills the
// buffer instead of submitting — matching Tab/Right-arrow behavior.
// This prevents accidental execution of destructive slash commands
// (/clear, /quit) and aligns with Claude Code's design: suggestion
// acceptance requires explicit Tab or arrow-key action.
if (buffer.text.length === 0 && availableSuggestion) {
followup.accept('enter', {
fallbackText: promptSuggestion ?? undefined,
});
return true;
}
if (buffer.text.trim()) {
Expand Down Expand Up @@ -1344,7 +1353,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
// Dismiss follow-up suggestion only on printable character input
if (
buffer.text.length === 0 &&
followup.state.isVisible &&
availableSuggestion &&
key.sequence &&
key.sequence.length === 1 &&
!key.ctrl &&
Expand Down Expand Up @@ -1425,7 +1434,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
enterBgDetailFromPanel,
setBgSelectedIndex,
followup,
availableSuggestion,
onPromptSuggestionDismiss,
promptSuggestion,
exportCompletion,
isHistoryRestoredText,
showCompletionSuggestions,
Expand Down Expand Up @@ -1578,9 +1589,17 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
// consumed (ACCEPT_SUGGESTION_REVERSE_SEARCH). When they are active with no
// matches, Tab is not consumed — so the bare `reverseSearchActive` /
// `commandSearchActive` flags are intentionally NOT included here.
// Mirror exactly when the Tab/Right/Enter handlers actually consume the key:
// the buffer must be empty and a suggestion must be available (either from the
// followup controller or the `promptSuggestion` prop after type-then-delete).
// Tying this to `buffer.text.length === 0` — the same gate the handlers use —
// keeps Windows Tab approval-mode cycling correct: as soon as the user types,
// this drops to false; deleting back to empty restores it.
const hasTabConsumer =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] hasTabConsumer manually inlines the equivalent of availableSuggestion instead of reusing it. The comment above says "Mirror exactly when the Tab/Right/Enter handlers actually consume the key" yet uses a different expression. If availableSuggestion semantics change in the future, this parallel expression must be updated independently.

Suggested change
const hasTabConsumer =
const hasTabConsumer =
shouldShowSuggestions ||
(buffer.text.length === 0 && Boolean(availableSuggestion)) ||
Boolean(completion.midInputGhostText?.acceptText);

— qwen3.7-max via Qwen Code /review

shouldShowSuggestions ||
(followup.state.isVisible && Boolean(followup.state.suggestion)) ||
(buffer.text.length === 0 &&
((followup.state.isVisible && Boolean(followup.state.suggestion)) ||
Boolean(promptSuggestion))) ||
Boolean(completion.midInputGhostText?.acceptText);

// Narrow signal — autocomplete dropdown only. Composer hides Footer /
Expand Down Expand Up @@ -1678,11 +1697,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
onSubmit={handleSubmitAndClear}
onKeypress={handleInput}
showCursor={showCursor}
placeholder={
followup.state.isVisible && followup.state.suggestion
? followup.state.suggestion
: placeholder
}
placeholder={availableSuggestion ?? placeholder}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Now that enableFollowupSuggestions defaults to on, every user renders the model-generated suggestion in the placeholder here. That text is influenceable through conversation history (tool / file / web output → prompt injection), and the only filter (getFilterReason, suggestionGenerator.ts:322) rejects newlines and * but not carriage returns, ESC / CSI escape sequences, or other C0 control bytes — which then reach the terminal verbatim via BaseTextInput's <Text>. By contrast the accept → buffer.insert path does strip them, so display and insert can disagree.

The render sink is pre-existing (main already showed followup.state.suggestion here), but flipping the default to on ships it to everyone, so it's worth hardening alongside this change. Defense-in-depth: strip control / ANSI characters at the source (in getFilterReason / the generator output) so displayed and inserted text always match. Low practical likelihood (the model would have to emit raw control bytes), hence Suggestion rather than blocker.

— claude-opus-4-8 via Claude Code /qreview

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in bb6eb3726. I considered both strip (sanitize in place, keep showing the cleaned suggestion) and reject (drop the whole suggestion via getFilterReason), and went with reject. Quick comparison:

Strip

  • 👍 Salvages a suggestion that only carries a stray control byte — slightly better recall.
  • 👎 getFilterReason is a pure classifier (returns a reason, never mutates its input). Stripping either changes that contract or needs a second sanitize pass.
  • 👎 To actually guarantee "display == insert", the same sanitized string has to flow to both the placeholder render and buffer.insert; otherwise they can still drift.
  • 👎 Renders a cleaned version of content that, when it contains raw control/ANSI bytes, is almost certainly garbage or injected.

Reject (chosen)

  • 👍 Consistent with the existing rules in getFilterReason (\n, *, too_long, …) — same shape, same contract, nothing mutates.
  • 👍 Fully resolves the display/insert concern by construction: a flagged suggestion is dropped, so nothing is rendered and nothing is inserted — they can't disagree.
  • 👍 Strictly safest: no control/ANSI byte ever reaches the terminal.
  • 👎 Discards a suggestion that might have been recoverable by stripping.

On balance reject wins here: the only thing strip buys us is salvaging a suggestion that contains control bytes — which in a 2-12 word hint is near-certainly noise/injection, so the lost-recall downside is negligible, while reject is simpler, matches the function's existing contract, and resolves the consistency issue more directly. Happy to switch to strip if you'd prefer to keep those suggestions.

prefix={prefixNode}
borderColor={borderColor}
topRightLabel={uiState.sessionName || undefined}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/ui/components/MainContent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ const createUIState = (overrides: Partial<UIState> = {}): UIState =>
sessionName: null,
setSessionName: vi.fn(),
promptSuggestion: null,
dismissPromptSuggestion: vi.fn(),
abortPromptSuggestion: vi.fn(),
isRewindSelectorOpen: false,
rewindEscPending: false,
...overrides,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/ui/contexts/UIStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export interface UIState {
// Prompt suggestion
promptSuggestion: string | null;
/** Dismiss prompt suggestion (clears state, aborts speculation) */
dismissPromptSuggestion: () => void;
abortPromptSuggestion: () => void;
// Rewind selector
isRewindSelectorOpen: boolean;
rewindEscPending: boolean;
Expand Down
Loading
Loading