fix: bootstrap embedded chat prompting state and review read-only handling
Pass initial subagent prompting settings into embedded chat URLs and views Prevent inactive embedded chats from stealing focus on load Treat review sessions as independent conversations when resolving prompt read-only state
This commit is contained in:
@@ -535,9 +535,10 @@ type ChatContainerProps = {
|
||||
active?: boolean;
|
||||
autoOpenDraft?: boolean;
|
||||
readOnly?: boolean;
|
||||
initialAllowPromptingSubagentSessions?: boolean;
|
||||
};
|
||||
|
||||
export const ChatContainer: React.FC<ChatContainerProps> = ({ active = true, autoOpenDraft = true, readOnly = false }) => {
|
||||
export const ChatContainer: React.FC<ChatContainerProps> = ({ active = true, autoOpenDraft = true, readOnly = false, initialAllowPromptingSubagentSessions }) => {
|
||||
const { t } = useI18n();
|
||||
// Session UI state
|
||||
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
||||
@@ -568,6 +569,7 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ active = true, aut
|
||||
const stickyUserHeader = useUIStore((state) => state.stickyUserHeader);
|
||||
const promptNavigatorEnabled = useUIStore((state) => state.promptNavigatorEnabled);
|
||||
const allowPromptingSubagentSessions = useUIStore((state) => state.allowPromptingSubagentSessions);
|
||||
const [embeddedAllowPrompting, setEmbeddedAllowPrompting] = React.useState(initialAllowPromptingSubagentSessions);
|
||||
const isTimelineDialogOpen = useUIStore((s) => s.isTimelineDialogOpen);
|
||||
const setTimelineDialogOpen = useUIStore((s) => s.setTimelineDialogOpen);
|
||||
|
||||
@@ -800,7 +802,11 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ active = true, aut
|
||||
{t('chat.container.returnToParent.label')}
|
||||
</Button>
|
||||
) : null;
|
||||
const promptReadOnly = resolveChatPromptReadOnly(currentSession, allowPromptingSubagentSessions, readOnly);
|
||||
const promptReadOnly = resolveChatPromptReadOnly(
|
||||
currentSession,
|
||||
embeddedAllowPrompting ?? allowPromptingSubagentSessions,
|
||||
readOnly,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
// VS Code/Cursor/Positron webviews delete window.parent (and window.top).
|
||||
@@ -813,6 +819,7 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ active = true, aut
|
||||
|
||||
const parentWindow = window.parent;
|
||||
const applySetting = (value: boolean) => {
|
||||
setEmbeddedAllowPrompting(value);
|
||||
useUIStore.getState().setAllowPromptingSubagentSessions(value);
|
||||
};
|
||||
const scopedWindow = window as typeof window & {
|
||||
@@ -1100,7 +1107,7 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ active = true, aut
|
||||
: 'flex-1 items-center justify-center bg-background px-0 pb-[6vh]'
|
||||
)}
|
||||
>
|
||||
{promptReadOnly ? <ReadOnlyPromptBanner /> : <ChatInput scrollToBottom={scrollToBottomOnSend} />}
|
||||
{promptReadOnly ? <ReadOnlyPromptBanner /> : <ChatInput active={active} scrollToBottom={scrollToBottomOnSend} />}
|
||||
</div>
|
||||
{workStatusOverlayMountable ? (
|
||||
<WorkStatusPanel
|
||||
@@ -1144,7 +1151,7 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ active = true, aut
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative z-10 bg-background">
|
||||
{promptReadOnly ? <ReadOnlyPromptBanner /> : <ChatInput scrollToBottom={scrollToBottomOnSend} />}
|
||||
{promptReadOnly ? <ReadOnlyPromptBanner /> : <ChatInput active={active} scrollToBottom={scrollToBottomOnSend} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1198,7 +1205,7 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ active = true, aut
|
||||
: 'bg-background'
|
||||
)}
|
||||
>
|
||||
{promptReadOnly ? <ReadOnlyPromptBanner /> : <ChatInput scrollToBottom={scrollToBottomOnSend} />}
|
||||
{promptReadOnly ? <ReadOnlyPromptBanner /> : <ChatInput active={active} scrollToBottom={scrollToBottomOnSend} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1233,7 +1240,7 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ active = true, aut
|
||||
: 'bg-background'
|
||||
)}
|
||||
>
|
||||
{promptReadOnly ? <ReadOnlyPromptBanner /> : <ChatInput scrollToBottom={scrollToBottomOnSend} />}
|
||||
{promptReadOnly ? <ReadOnlyPromptBanner /> : <ChatInput active={active} scrollToBottom={scrollToBottomOnSend} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1291,7 +1298,7 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ active = true, aut
|
||||
onClick={navigation.resumeToLatest}
|
||||
/>
|
||||
)}
|
||||
{promptReadOnly ? <ReadOnlyPromptBanner /> : <ChatInput scrollToBottom={scrollToBottomOnSend} />}
|
||||
{promptReadOnly ? <ReadOnlyPromptBanner /> : <ChatInput active={active} scrollToBottom={scrollToBottomOnSend} />}
|
||||
</div>
|
||||
|
||||
{/* Inside the chat column, not beside it: as a row sibling it took
|
||||
|
||||
@@ -221,6 +221,7 @@ const MemoStatusRow = React.memo(StatusRow);
|
||||
interface ChatInputProps {
|
||||
onOpenSettings?: () => void;
|
||||
scrollToBottom?: () => void;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
const resolveChatDraftIdentity = (sessionId: string | null): ChatDraftIdentity | null => {
|
||||
@@ -234,7 +235,7 @@ const resolveChatDraftIdentity = (sessionId: string | null): ChatDraftIdentity |
|
||||
return createChatDraftIdentity(getRuntimeKey(), directory, sessionId);
|
||||
};
|
||||
|
||||
const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBottom }) => {
|
||||
const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBottom, active = true }) => {
|
||||
const { t } = useI18n();
|
||||
// Track if we restored a draft on mount (for text selection)
|
||||
const initialDraftRef = React.useRef<string | null>(null);
|
||||
@@ -2039,10 +2040,10 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
if (currentSessionId && composerRef.current && !isMobile) {
|
||||
if (active && currentSessionId && composerRef.current && !isMobile) {
|
||||
composerRef.current.focus();
|
||||
}
|
||||
}, [currentSessionId, isMobile]);
|
||||
}, [active, currentSessionId, isMobile]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isMobile) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from 'bun:test';
|
||||
import type { Session } from '@opencode-ai/sdk/v2';
|
||||
|
||||
import { resolveChatPromptReadOnly } from './chatPromptReadOnly';
|
||||
import { withReviewSessionMarker } from '@/lib/sessionReviewMetadata';
|
||||
|
||||
const session = (parentID?: string): Session => ({
|
||||
id: 'session',
|
||||
@@ -27,4 +28,14 @@ describe('resolveChatPromptReadOnly', () => {
|
||||
expect(resolveChatPromptReadOnly(session(), true, true)).toBe(true);
|
||||
expect(resolveChatPromptReadOnly(session(), true, false)).toBe(false);
|
||||
});
|
||||
|
||||
test('treats a marked code review as an independent session even with a stale parent ID', () => {
|
||||
const reviewSession = {
|
||||
...session('original'),
|
||||
metadata: withReviewSessionMarker({}, 'original'),
|
||||
} as Session;
|
||||
|
||||
expect(resolveChatPromptReadOnly(reviewSession, false, false)).toBe(false);
|
||||
expect(resolveChatPromptReadOnly(reviewSession, true, true)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import type { Session } from '@opencode-ai/sdk/v2';
|
||||
import { isReviewSession } from '@/lib/sessionReviewMetadata';
|
||||
|
||||
export const resolveChatPromptReadOnly = (
|
||||
session: Session | null | undefined,
|
||||
allowPromptingSubagentSessions: boolean,
|
||||
readOnly: boolean,
|
||||
): boolean => {
|
||||
// Review sessions are independent conversations even if an older server or
|
||||
// cached record still carries parentID. Their explicit metadata is the
|
||||
// authority; only the surface itself may make them read-only.
|
||||
if (isReviewSession(session)) {
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
if (session?.parentID) {
|
||||
return !allowPromptingSubagentSessions;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user