fix(vscode): hide subagent promotion in readonly views

This commit is contained in:
marius-kilocode
2026-08-25 11:06:34 +02:00
parent 11e4a38ad9
commit 4c0767c527
7 changed files with 63 additions and 11 deletions
@@ -163,6 +163,7 @@ export interface MessagePartProps {
working?: boolean
feedback?: MessageFeedbackControls
throughput?: JSX.Element
readonly?: boolean
}
export type PartComponent = Component<MessagePartProps>
@@ -1044,6 +1045,7 @@ export function Part(props: MessagePartProps) {
working={props.working}
feedback={props.feedback}
throughput={props.throughput}
readonly={props.readonly}
/>
</Show>
)
@@ -1068,6 +1070,7 @@ export interface ToolProps {
locked?: boolean
animate?: boolean
reveal?: boolean
readonly?: boolean
}
export type ToolComponent = Component<ToolProps>
@@ -1267,6 +1270,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
forceOpen={props.forceOpen}
animate
reveal={props.animate}
readonly={props.readonly}
/>
)
}
@@ -1344,6 +1348,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
forceOpenFile={props.forceOpenFile}
animate
reveal={props.animate}
readonly={props.readonly}
/>
</ToolApprovalProvider>
</Match>
@@ -4,6 +4,7 @@ import {
backgroundJobAgents,
showBackgroundAgent,
} from "../../webview-ui/src/components/chat/background-agents"
import { childForeground, showChildPromotion } from "../../webview-ui/src/components/chat/task-tool-state"
import type {
BackgroundJobInfo,
PermissionRequest,
@@ -73,6 +74,22 @@ describe("backgroundAgents", () => {
expect(backgroundAgents([taskPart({ child: "ses_child" })], { ses_child: busy })).toEqual([])
})
it("identifies each parallel foreground child independently", () => {
const status = { ses_a: busy, ses_b: busy }
expect(childForeground("ses_a", {}, {}, status)).toBe(true)
expect(childForeground("ses_b", {}, {}, status)).toBe(true)
expect(childForeground("ses_a", { background: true }, {}, status)).toBe(false)
expect(childForeground("ses_b", {}, { background: true }, status)).toBe(false)
expect(childForeground("ses_a", {}, {}, { ses_a: idle })).toBe(false)
expect(childForeground("ses_a", {}, {}, { ses_a: { type: "retry", attempt: 1, message: "retry", next: 1 } })).toBe(
true,
)
expect(childForeground(undefined, {}, {}, status)).toBe(false)
expect(showChildPromotion("ses_a", {}, {}, status, false)).toBe(true)
expect(showChildPromotion("ses_a", {}, {}, status, true)).toBe(false)
})
it("ignores agents whose session is no longer working", () => {
const tools = [taskPart({ child: "ses_child", background: true })]
@@ -114,6 +114,7 @@ interface AssistantMessageProps {
forceOpenFile?: string
/** Part behind the currently hovered/focused task-timeline bar, if any. */
highlight?: () => TimelineHighlight | undefined
readonly?: boolean
}
type ToolStateProps = {
@@ -339,6 +340,7 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
reasoningAutoCollapse={display.reasoningAutoCollapse()}
feedback={props.feedback}
throughput={throughputEl()}
readonly={props.readonly}
animate={
part.type === "tool" &&
((part as unknown as ToolPart).state?.status === "pending" ||
@@ -1332,6 +1332,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
activeSearch={activeKey() === row.key}
activeSearchPartID={activeKey() === row.key ? activeMatch()?.partId : undefined}
activeSearchPartFile={activeKey() === row.key ? activeMatch()?.partFile : undefined}
readonly={props.readonly}
/>
)}
</Virtualizer>
@@ -1345,6 +1346,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
activeSearch={activeKey() === key}
activeSearchPartID={activeKey() === key ? activeMatch()?.partId : undefined}
activeSearchPartFile={activeKey() === key ? activeMatch()?.partFile : undefined}
readonly={props.readonly}
/>
)}
</For>
@@ -1360,6 +1362,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
activeSearch={activeKey() === row.key}
activeSearchPartID={activeKey() === row.key ? activeMatch()?.partId : undefined}
activeSearchPartFile={activeKey() === row.key ? activeMatch()?.partFile : undefined}
readonly={props.readonly}
/>
)}
</For>
@@ -21,7 +21,7 @@ import { useVSCode } from "../../context/vscode"
import { useWorktreeMode } from "../../context/worktree-mode"
import { childID } from "../../context/session-utils"
import { openSubagent } from "./open-subagent"
import { taskResult, taskRunning, taskVisible } from "./task-tool-state"
import { showChildPromotion, taskResult, taskRunning, taskVisible } from "./task-tool-state"
const TaskToolRenderer: Component<ToolProps> = (props) => {
const i18n = useI18n()
@@ -38,15 +38,15 @@ const TaskToolRenderer: Component<ToolProps> = (props) => {
state: { metadata: props.metadata as { sessionId?: string } },
})
const childForeground = createMemo(() => {
const id = childSessionId()
if (!id) return false
const part = props.partMetadata as { background?: boolean } | undefined
const state = props.metadata as { background?: boolean } | undefined
if (part?.background === true || state?.background === true) return false
const status = session.allStatusMap()[id]
return status?.type === "busy" || status?.type === "retry"
})
const promotable = createMemo(() =>
showChildPromotion(
childSessionId(),
props.partMetadata as Record<string, unknown> | undefined,
props.metadata as Record<string, unknown> | undefined,
session.allStatusMap(),
props.readonly,
),
)
const running = createMemo(() => taskRunning(props.status))
// BasicTool's forceOpen effect only fires onOpenChange on a false->true
@@ -167,7 +167,7 @@ const TaskToolRenderer: Component<ToolProps> = (props) => {
</Show>
</div>
<Show when={childSessionId()}>
<Show when={childForeground()}>
<Show when={promotable()}>
<IconButton
icon="play"
size="small"
@@ -26,6 +26,7 @@ interface TranscriptRowViewProps {
activeSearchPartID?: string
/** For a multi-file apply_patch match, the specific file within that part. */
activeSearchPartFile?: string
readonly?: boolean
}
export const TranscriptRowView: Component<TranscriptRowViewProps> = (props) => {
@@ -92,6 +93,7 @@ export const TranscriptRowView: Component<TranscriptRowViewProps> = (props) => {
forceOpenPartID={props.activeSearchPartID}
forceOpenFile={props.activeSearchPartFile}
highlight={props.highlight}
readonly={props.readonly}
feedback={{
enabled: feedback.telemetryEnabled(),
rating: feedback.getRating(row().message.id),
@@ -1,7 +1,30 @@
import type { SessionStatusInfo } from "../../types/messages"
export function taskRunning(status: string | undefined) {
return status === "pending" || status === "running"
}
export function childForeground(
id: string | undefined,
part: Record<string, unknown> | undefined,
state: Record<string, unknown> | undefined,
status: Record<string, SessionStatusInfo>,
) {
if (!id) return false
if (part?.background === true || state?.background === true) return false
return status[id]?.type === "busy" || status[id]?.type === "retry"
}
export function showChildPromotion(
id: string | undefined,
part: Record<string, unknown> | undefined,
state: Record<string, unknown> | undefined,
status: Record<string, SessionStatusInfo>,
readonly: boolean | undefined,
) {
return !readonly && childForeground(id, part, state, status)
}
export function taskVisible(open: boolean | undefined, id: string | undefined) {
return open ? id : undefined
}