Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 23 additions & 1 deletion apps/app/src/components/promptbox/FollowUpPromptBox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const mocks = vi.hoisted(() => {
isPointerCoarse: false,
scrollToBottom: vi.fn(),
permissionModePicker: vi.fn(),
voiceState: "idle" as "idle" | "recording" | "transcribing" | "error",
};
return Object.assign(values, {});
});
Expand Down Expand Up @@ -62,6 +63,7 @@ vi.mock("@/components/promptbox/PromptBoxInternal", () => ({
zenMode,
heightAnimationKey,
minHeight,
voice,
}: {
footerStart?: ReactNode;
compact?: {
Expand All @@ -81,13 +83,15 @@ vi.mock("@/components/promptbox/PromptBoxInternal", () => ({
zenMode?: { resetKey: string | number };
heightAnimationKey?: string | number;
minHeight?: number;
voice?: { state: "idle" | "recording" | "transcribing" | "error" };
}) => (
<div
data-testid="prompt-box"
data-compact={compact?.isCompact}
data-zen-reset-key={zenMode?.resetKey}
data-height-animation-key={heightAnimationKey}
data-min-height={minHeight}
data-voice-state={voice?.state}
data-plugin-customizations-suppressed={
suppressPluginComposerCustomizations ? "true" : "false"
}
Expand Down Expand Up @@ -133,7 +137,7 @@ vi.mock("@/components/promptbox/PromptBoxInternal", () => ({

vi.mock("@/components/promptbox/usePromptVoice", () => ({
usePromptVoice: () => ({
state: "idle",
state: mocks.voiceState,
isSupported: false,
stream: null,
start: vi.fn(),
Expand Down Expand Up @@ -259,6 +263,7 @@ afterEach(() => {
beforeEach(() => {
mocks.isCompactViewport = false;
mocks.isPointerCoarse = false;
mocks.voiceState = "idle";
resizeObserverCallback = null;
vi.stubGlobal(
"ResizeObserver",
Expand Down Expand Up @@ -977,6 +982,23 @@ describe("FollowUpPromptBox", () => {
expect(screen.getByText("Local environment")).toBeTruthy();
});

it.each(["recording", "transcribing"] as const)(
"keeps the status footer while the prompt box handles voice controls during %s",
(state) => {
mocks.voiceState = state;
const props = createFollowUpPromptBoxProps({ kind: "ready" });
props.environmentSummary = <span>Local environment</span>;

render(<FollowUpPromptBox {...props} />);

expect(screen.getByTestId("prompt-box").dataset.voiceState).toBe(state);
expect(
document.querySelector("[data-follow-up-composer-footer]"),
).toBeTruthy();
expect(screen.getByText("Local environment")).toBeTruthy();
},
);

it("exposes focus state so narrow prompt containers can expand", async () => {
render(
<>
Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/components/promptbox/NewThreadPromptBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ export const NewThreadPromptBoxUI = memo(function NewThreadPromptBoxUI({
promptBoxRef.current?.insertTextAtCursor(text);
},
getTextBeforeCursor: () => promptBoxRef.current?.getTextBeforeCursor(),
playVoiceCompletionTransition: () =>
promptBoxRef.current?.playVoiceCompletionTransition() ??
Promise.resolve(),
}),
[],
);
Expand Down
36 changes: 36 additions & 0 deletions apps/app/src/components/promptbox/PromptBoxInternal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,27 @@ function RecordingActiveRow() {
);
}

function RecordingWithExistingDraftRow() {
const { value, mentionRanges, onChange } = useControlledValue(
"Keep this existing prompt visible while I dictate the rest of the request.",
);
return (
<PromptBoxInternal
value={value}
mentionRanges={mentionRanges}
onChange={onChange}
onSubmit={noop}
typeahead={makeTypeahead()}
mentionMenuPlacement="bottom"
attachments={makeAttachments()}
history={baseHistory}
submission={makeSubmission()}
voice={recordingVoice}
footerStart={<ExecutionControls {...mockExecution} />}
/>
);
}

function RecordingProcessingRow() {
const { value, mentionRanges, onChange } = useControlledValue("");
return (
Expand Down Expand Up @@ -1011,6 +1032,21 @@ export function PromptActions() {
);
}

export function VoiceActionRowRecommendation() {
return (
<StoryCard>
<StoryRow
label="recording with existing draft"
hint="The editor stays visible while the bottom action row becomes cancel, waveform, and confirm."
>
<div className="w-full max-w-4xl">
<RecordingWithExistingDraftRow />
</div>
</StoryRow>
</StoryCard>
);
}

export function Overview() {
return (
<StoryCard>
Expand Down
Loading