= ({ active = true, aut
- {promptReadOnly ? : }
+ {promptReadOnly ? : }
);
@@ -1198,7 +1205,7 @@ export const ChatContainer: React.FC = ({ active = true, aut
: 'bg-background'
)}
>
- {promptReadOnly ? : }
+ {promptReadOnly ? : }
);
@@ -1233,7 +1240,7 @@ export const ChatContainer: React.FC = ({ active = true, aut
: 'bg-background'
)}
>
- {promptReadOnly ? : }
+ {promptReadOnly ? : }
);
@@ -1291,7 +1298,7 @@ export const ChatContainer: React.FC = ({ active = true, aut
onClick={navigation.resumeToLatest}
/>
)}
- {promptReadOnly ? : }
+ {promptReadOnly ? : }
{/* Inside the chat column, not beside it: as a row sibling it took
diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx
index 5ff1d8ab..1df58ebb 100644
--- a/packages/ui/src/components/chat/ChatInput.tsx
+++ b/packages/ui/src/components/chat/ChatInput.tsx
@@ -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 = ({ onOpenSettings, scrollToBottom }) => {
+const ChatInputComponent: React.FC = ({ onOpenSettings, scrollToBottom, active = true }) => {
const { t } = useI18n();
// Track if we restored a draft on mount (for text selection)
const initialDraftRef = React.useRef(null);
@@ -2039,10 +2040,10 @@ const ChatInputComponent: React.FC = ({ 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) {
diff --git a/packages/ui/src/components/chat/chatPromptReadOnly.test.ts b/packages/ui/src/components/chat/chatPromptReadOnly.test.ts
index 38ccc362..c78f7afc 100644
--- a/packages/ui/src/components/chat/chatPromptReadOnly.test.ts
+++ b/packages/ui/src/components/chat/chatPromptReadOnly.test.ts
@@ -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);
+ });
});
diff --git a/packages/ui/src/components/chat/chatPromptReadOnly.ts b/packages/ui/src/components/chat/chatPromptReadOnly.ts
index 1fcc78af..2e1f5c2e 100644
--- a/packages/ui/src/components/chat/chatPromptReadOnly.ts
+++ b/packages/ui/src/components/chat/chatPromptReadOnly.ts
@@ -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;
}
diff --git a/packages/ui/src/components/layout/ContextPanel.tsx b/packages/ui/src/components/layout/ContextPanel.tsx
index 6b173209..43872d90 100644
--- a/packages/ui/src/components/layout/ContextPanel.tsx
+++ b/packages/ui/src/components/layout/ContextPanel.tsx
@@ -2528,8 +2528,8 @@ export const ContextPanel: React.FC = () => {
lightThemeId,
darkThemeId,
currentTheme,
- });
- }, [currentTheme, darkThemeId, directoryKey, lightThemeId, themeMode]);
+ }, { allowPromptingSubagentSessions });
+ }, [allowPromptingSubagentSessions, currentTheme, darkThemeId, directoryKey, lightThemeId, themeMode]);
const activeChatSrc = activeChatTab && activeChatSessionID
? getEmbeddedChatSrc(activeChatTab.id, activeChatSessionID, activeChatTab.readOnly)
diff --git a/packages/ui/src/components/layout/contextPanelEmbeddedChat.test.ts b/packages/ui/src/components/layout/contextPanelEmbeddedChat.test.ts
index 08992565..1c2cb866 100644
--- a/packages/ui/src/components/layout/contextPanelEmbeddedChat.test.ts
+++ b/packages/ui/src/components/layout/contextPanelEmbeddedChat.test.ts
@@ -128,6 +128,17 @@ describe('embedded session chat URL', () => {
expect(new URL(second).searchParams.get('themeVariant')).toBe('dark');
});
+ test('bootstraps subagent prompting before the embedded chat first renders', () => {
+ const src = buildEmbeddedSessionChatURL('ses_1', '/repo', false, {
+ mode: 'system',
+ lightThemeId: 'light-a',
+ darkThemeId: 'dark-a',
+ currentTheme: makeTheme('dark-a', 'dark'),
+ }, { allowPromptingSubagentSessions: true });
+
+ expect(new URL(src).searchParams.get('allowPromptingSubagentSessions')).toBe('1');
+ });
+
test('rebuilds cached src when readOnly changes for an existing tab', () => {
const cache = new Map();
const theme = {
diff --git a/packages/ui/src/components/layout/contextPanelEmbeddedChat.ts b/packages/ui/src/components/layout/contextPanelEmbeddedChat.ts
index 57789111..05068ca6 100644
--- a/packages/ui/src/components/layout/contextPanelEmbeddedChat.ts
+++ b/packages/ui/src/components/layout/contextPanelEmbeddedChat.ts
@@ -8,6 +8,10 @@ export type EmbeddedSessionChatThemeBootstrap = {
currentTheme: Theme;
};
+export type EmbeddedSessionChatSettingsBootstrap = {
+ allowPromptingSubagentSessions: boolean;
+};
+
export type EmbeddedSessionChatURLCacheEntry = {
signature: string;
src: string;
@@ -111,6 +115,7 @@ export const buildEmbeddedSessionChatURL = (
directory: string | null,
readOnly: boolean,
theme: EmbeddedSessionChatThemeBootstrap,
+ settings?: EmbeddedSessionChatSettingsBootstrap,
): string => {
if (typeof window === 'undefined') {
return '';
@@ -134,6 +139,9 @@ export const buildEmbeddedSessionChatURL = (
url.searchParams.set('lightThemeId', theme.lightThemeId);
url.searchParams.set('darkThemeId', theme.darkThemeId);
url.searchParams.set('themeVariant', theme.currentTheme.metadata.variant === 'dark' ? 'dark' : 'light');
+ if (settings) {
+ url.searchParams.set('allowPromptingSubagentSessions', settings.allowPromptingSubagentSessions ? '1' : '0');
+ }
url.hash = '';
return url.toString();
@@ -146,6 +154,7 @@ export const getOrCreateEmbeddedSessionChatURL = (
directory: string | null,
readOnly: boolean,
theme: EmbeddedSessionChatThemeBootstrap,
+ settings?: EmbeddedSessionChatSettingsBootstrap,
): string => {
const signature = buildEmbeddedSessionChatURLSignature(sessionID, directory, readOnly);
const existing = cache.get(tabID);
@@ -153,7 +162,7 @@ export const getOrCreateEmbeddedSessionChatURL = (
return existing.src;
}
- const src = buildEmbeddedSessionChatURL(sessionID, directory, readOnly, theme);
+ const src = buildEmbeddedSessionChatURL(sessionID, directory, readOnly, theme, settings);
cache.set(tabID, { signature, src });
return src;
};
diff --git a/packages/ui/src/components/views/ChatView.tsx b/packages/ui/src/components/views/ChatView.tsx
index 152998f4..bcba8dc7 100644
--- a/packages/ui/src/components/views/ChatView.tsx
+++ b/packages/ui/src/components/views/ChatView.tsx
@@ -6,14 +6,19 @@ import { useSessionUIStore } from '@/sync/session-ui-store';
type ChatViewProps = {
active?: boolean;
readOnly?: boolean;
+ initialAllowPromptingSubagentSessions?: boolean;
};
-export const ChatView: React.FC = ({ active = true, readOnly = false }) => {
+export const ChatView: React.FC = ({ active = true, readOnly = false, initialAllowPromptingSubagentSessions }) => {
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
return (
-
+
);
};