Skip to content

Commit 8d6d633

Browse files
Preserve unsent chat drafts while permission prompts are active (#22)
* Initial plan * fix: preserve chat draft during permission prompts Co-authored-by: OpenSource03 <29690431+OpenSource03@users.noreply.github.com> * fix: keep chat composer mounted during permission prompts Co-authored-by: OpenSource03 <29690431+OpenSource03@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: OpenSource03 <29690431+OpenSource03@users.noreply.github.com>
1 parent 6112d06 commit 8d6d633

File tree

3 files changed

+146
-47
lines changed

3 files changed

+146
-47
lines changed

src/components/AppLayout.tsx

Lines changed: 40 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ import { AppSidebar } from "./AppSidebar";
1919
import { ChatHeader } from "./ChatHeader";
2020
import { ChatSearchBar } from "./ChatSearchBar";
2121
import { ChatView } from "./ChatView";
22-
import { InputBar } from "./InputBar";
23-
import { PermissionPrompt } from "./PermissionPrompt";
22+
import { BottomComposer } from "./BottomComposer";
2423
import { TodoPanel } from "./TodoPanel";
2524
import { BackgroundAgentsPanel } from "./BackgroundAgentsPanel";
2625
import { ToolPicker } from "./ToolPicker";
@@ -529,51 +528,45 @@ Link: ${issue.url}`;
529528
}}
530529
/>
531530
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10">
532-
{manager.pendingPermission ? (
533-
<PermissionPrompt
534-
key={manager.pendingPermission.requestId}
535-
request={manager.pendingPermission}
536-
onRespond={manager.respondPermission}
537-
/>
538-
) : (
539-
<InputBar
540-
onSend={wrappedHandleSend}
541-
onStop={handleStop}
542-
isProcessing={manager.isProcessing}
543-
queuedCount={manager.queuedCount}
544-
model={settings.model}
545-
claudeEffort={settings.claudeEffort}
546-
planMode={settings.planMode}
547-
permissionMode={settings.permissionMode}
548-
onModelChange={handleModelChange}
549-
onClaudeModelEffortChange={handleClaudeModelEffortChange}
550-
onPlanModeChange={handlePlanModeChange}
551-
onPermissionModeChange={handlePermissionModeChange}
552-
projectPath={activeProjectPath}
553-
contextUsage={manager.contextUsage}
554-
isCompacting={manager.isCompacting}
555-
onCompact={manager.compact}
556-
agents={agents}
557-
selectedAgent={selectedAgent}
558-
onAgentChange={handleAgentChange}
559-
slashCommands={manager.slashCommands}
560-
acpConfigOptions={manager.acpConfigOptions}
561-
acpConfigOptionsLoading={manager.acpConfigOptionsLoading}
562-
onACPConfigChange={manager.setACPConfig}
563-
acpPermissionBehavior={settings.acpPermissionBehavior}
564-
onAcpPermissionBehaviorChange={settings.setAcpPermissionBehavior}
565-
supportedModels={manager.supportedModels}
566-
codexModelsLoadingMessage={manager.codexModelsLoadingMessage}
567-
codexEffort={manager.codexEffort}
568-
onCodexEffortChange={manager.setCodexEffort}
569-
codexModelData={manager.codexRawModels}
570-
grabbedElements={grabbedElements}
571-
onRemoveGrabbedElement={handleRemoveGrabbedElement}
572-
lockedEngine={lockedEngine}
573-
lockedAgentId={lockedAgentId}
574-
isIslandLayout={isIsland}
575-
/>
576-
)}
531+
<BottomComposer
532+
pendingPermission={manager.pendingPermission}
533+
onRespondPermission={manager.respondPermission}
534+
onSend={wrappedHandleSend}
535+
onStop={handleStop}
536+
isProcessing={manager.isProcessing}
537+
queuedCount={manager.queuedCount}
538+
model={settings.model}
539+
claudeEffort={settings.claudeEffort}
540+
planMode={settings.planMode}
541+
permissionMode={settings.permissionMode}
542+
onModelChange={handleModelChange}
543+
onClaudeModelEffortChange={handleClaudeModelEffortChange}
544+
onPlanModeChange={handlePlanModeChange}
545+
onPermissionModeChange={handlePermissionModeChange}
546+
projectPath={activeProjectPath}
547+
contextUsage={manager.contextUsage}
548+
isCompacting={manager.isCompacting}
549+
onCompact={manager.compact}
550+
agents={agents}
551+
selectedAgent={selectedAgent}
552+
onAgentChange={handleAgentChange}
553+
slashCommands={manager.slashCommands}
554+
acpConfigOptions={manager.acpConfigOptions}
555+
acpConfigOptionsLoading={manager.acpConfigOptionsLoading}
556+
onACPConfigChange={manager.setACPConfig}
557+
acpPermissionBehavior={settings.acpPermissionBehavior}
558+
onAcpPermissionBehaviorChange={settings.setAcpPermissionBehavior}
559+
supportedModels={manager.supportedModels}
560+
codexModelsLoadingMessage={manager.codexModelsLoadingMessage}
561+
codexEffort={manager.codexEffort}
562+
onCodexEffortChange={manager.setCodexEffort}
563+
codexModelData={manager.codexRawModels}
564+
grabbedElements={grabbedElements}
565+
onRemoveGrabbedElement={handleRemoveGrabbedElement}
566+
lockedEngine={lockedEngine}
567+
lockedAgentId={lockedAgentId}
568+
isIslandLayout={isIsland}
569+
/>
577570
</div>
578571
</>
579572
) : (
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { renderToStaticMarkup } from "react-dom/server";
2+
import { describe, expect, it, vi } from "vitest";
3+
import type { ComponentProps } from "react";
4+
import { BottomComposer } from "./BottomComposer";
5+
6+
vi.mock("./InputBar", () => ({
7+
InputBar: () => <div data-testid="input-bar">input-bar</div>,
8+
}));
9+
10+
vi.mock("./PermissionPrompt", () => ({
11+
PermissionPrompt: ({ request }: { request: { requestId: string } }) => (
12+
<div data-testid="permission-prompt">{request.requestId}</div>
13+
),
14+
}));
15+
16+
type BottomComposerProps = ComponentProps<typeof BottomComposer>;
17+
18+
function createProps(
19+
overrides: Partial<BottomComposerProps> = {},
20+
): BottomComposerProps {
21+
return {
22+
pendingPermission: null,
23+
onRespondPermission: vi.fn(),
24+
onSend: vi.fn(),
25+
onStop: vi.fn(),
26+
isProcessing: false,
27+
model: "claude-sonnet-4",
28+
claudeEffort: "high",
29+
planMode: false,
30+
permissionMode: "default",
31+
onModelChange: vi.fn(),
32+
onClaudeModelEffortChange: vi.fn(),
33+
onPlanModeChange: vi.fn(),
34+
onPermissionModeChange: vi.fn(),
35+
queuedCount: 0,
36+
...overrides,
37+
};
38+
}
39+
40+
describe("BottomComposer", () => {
41+
it("keeps the input bar mounted when there is no pending permission", () => {
42+
const html = renderToStaticMarkup(<BottomComposer {...createProps()} />);
43+
44+
expect(html).toContain("aria-hidden=\"false\"");
45+
expect(html).toContain("data-testid=\"input-bar\"");
46+
expect(html).not.toContain("data-testid=\"permission-prompt\"");
47+
});
48+
49+
it("keeps the input bar mounted while also rendering the permission prompt", () => {
50+
const html = renderToStaticMarkup(
51+
<BottomComposer
52+
{...createProps({
53+
pendingPermission: {
54+
requestId: "req-1",
55+
toolName: "bash",
56+
toolInput: {},
57+
toolUseId: "tool-1",
58+
},
59+
})}
60+
/>,
61+
);
62+
63+
expect(html).toContain("data-testid=\"permission-prompt\"");
64+
expect(html).toContain("req-1");
65+
expect(html).toContain("aria-hidden=\"true\"");
66+
expect(html).toContain("inert=\"\"");
67+
expect(html).toContain("data-testid=\"input-bar\"");
68+
});
69+
});

src/components/BottomComposer.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { ComponentProps } from "react";
2+
import { InputBar } from "./InputBar";
3+
import { PermissionPrompt } from "./PermissionPrompt";
4+
5+
type InputBarProps = ComponentProps<typeof InputBar>;
6+
type PermissionPromptProps = ComponentProps<typeof PermissionPrompt>;
7+
8+
interface BottomComposerProps extends InputBarProps {
9+
pendingPermission: PermissionPromptProps["request"] | null;
10+
onRespondPermission: PermissionPromptProps["onRespond"];
11+
}
12+
13+
export function BottomComposer({
14+
pendingPermission,
15+
onRespondPermission,
16+
...inputBarProps
17+
}: BottomComposerProps) {
18+
const hasPendingPermission = !!pendingPermission;
19+
20+
return (
21+
<>
22+
{pendingPermission ? (
23+
<PermissionPrompt
24+
key={pendingPermission.requestId}
25+
request={pendingPermission}
26+
onRespond={onRespondPermission}
27+
/>
28+
) : null}
29+
<div
30+
aria-hidden={hasPendingPermission}
31+
inert={hasPendingPermission || undefined}
32+
>
33+
<InputBar {...inputBarProps} />
34+
</div>
35+
</>
36+
);
37+
}

0 commit comments

Comments
 (0)