Merge pull request #416 from znygugeyx-ctrl/feat/subagent-fork-render

feat: 参考 claude code 官方实现,改进 sub agent 以及 fork agent 的渲染方式
This commit is contained in:
znygugeyx-ctrl
2026-05-06 09:57:52 +08:00
committed by GitHub
parent c4e9efb7a8
commit 5c107e5f8c
11 changed files with 488 additions and 27 deletions

View File

@@ -244,7 +244,14 @@ import {
formatCommandInputTags,
} from '../utils/messages.js';
import { generateSessionTitle } from '../utils/sessionTitle.js';
import { BASH_INPUT_TAG, COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG, LOCAL_COMMAND_STDOUT_TAG } from '../constants/xml.js';
import {
BASH_INPUT_TAG,
COMMAND_MESSAGE_TAG,
COMMAND_NAME_TAG,
FORK_BOILERPLATE_TAG,
LOCAL_COMMAND_STDOUT_TAG,
} from '../constants/xml.js';
import { FORK_SUBAGENT_TYPE } from '@claude-code-best/builtin-tools/tools/AgentTool/forkSubagent.js';
import { escapeXml } from '../utils/xml.js';
import type { ThinkingConfig } from '../utils/thinking.js';
import { gracefulShutdownSync } from '../utils/gracefulShutdown.js';
@@ -336,6 +343,7 @@ import {
import { isBgSession, updateSessionName, updateSessionActivity } from '../utils/concurrentSessions.js';
import { isInProcessTeammateTask, type InProcessTeammateTaskState } from '../tasks/InProcessTeammateTask/types.js';
import { restoreRemoteAgentTasks } from '../tasks/RemoteAgentTask/RemoteAgentTask.js';
import { BackgroundAgentSelector } from '../components/tasks/BackgroundAgentSelector.js';
import { useInboxPoller } from '../hooks/useInboxPoller.js';
// Dead code elimination: conditional import for loop mode
/* eslint-disable @typescript-eslint/no-require-imports */
@@ -800,6 +808,21 @@ export type Props = {
export type Screen = 'prompt' | 'transcript';
// Boilerplate carrier lives in a mixed user message ([tool_result..., text])
// that AgentTool/forkSubagent.buildForkedMessages emits as the fork child's
// first user turn. The text block wraps <FORK_BOILERPLATE_TAG>...</..> + the
// user prompt; tool_result siblings keep the parent's tool calls closed.
const FORK_BOILERPLATE_OPEN_TAG = `<${FORK_BOILERPLATE_TAG}>`;
function isForkBoilerplateTextBlock(block: { type: string; text?: string }): boolean {
return block.type === 'text' && typeof block.text === 'string' && block.text.includes(FORK_BOILERPLATE_OPEN_TAG);
}
function isForkBoilerplateMessage(message: MessageType): boolean {
if (message.type !== 'user' || !Array.isArray(message.message?.content)) return false;
return message.message.content.some(isForkBoilerplateTextBlock);
}
export function REPL({
commands: initialCommands,
debug,
@@ -5548,8 +5571,72 @@ export function REPL({
const usesSyncMessages = showStreamingText || !isLoading;
// When viewing an agent, never fall through to leader — empty until
// bootstrap/stream fills. Closes the see-leader-type-agent footgun.
const rawAgentMessages = viewedAgentTask?.messages;
// Fork sidechain encodes the user prompt inside a mixed user message alongside
// tool_result blocks; surface the prompt as a standalone bubble and strip the
// boilerplate text from its original carrier while preserving tool_results.
const displayedAgentMessages = useMemo(() => {
if (!viewedAgentTask) return undefined;
const agentMessages = rawAgentMessages ?? [];
if (
!isLocalAgentTask(viewedAgentTask) ||
viewedAgentTask.agentType !== FORK_SUBAGENT_TYPE ||
!viewedAgentTask.prompt
) {
return agentMessages;
}
// Single pass: locate boilerplate carrier, check whether the prompt text is
// already present elsewhere, and find the fallback insertion point (after
// the last parent assistant tool_use).
const trimmedPrompt = viewedAgentTask.prompt.trim();
let boilerplateIndex = -1;
let lastAssistantToolUseIndex = -1;
let promptAlreadyRendered = false;
for (let i = 0; i < agentMessages.length; i++) {
const m = agentMessages[i]!;
if (m.type === 'user' && Array.isArray(m.message?.content)) {
const hasBoilerplate = m.message.content.some(isForkBoilerplateTextBlock);
if (hasBoilerplate) {
boilerplateIndex = i;
} else if (!promptAlreadyRendered) {
const firstText = m.message.content.find(b => b.type === 'text' && typeof b.text === 'string') as
| { type: 'text'; text: string }
| undefined;
if (firstText && firstText.text.trim() === trimmedPrompt) promptAlreadyRendered = true;
}
continue;
}
if (m.type === 'assistant' && Array.isArray(m.message?.content)) {
if (m.message.content.some(b => b.type === 'tool_use')) lastAssistantToolUseIndex = i;
}
}
const stripped =
boilerplateIndex === -1
? agentMessages
: agentMessages.map((m, i) => {
if (i !== boilerplateIndex) return m;
if (!Array.isArray(m.message?.content)) return m;
return {
...m,
message: {
...m.message,
content: m.message.content.filter(b => !isForkBoilerplateTextBlock(b)),
},
};
});
if (promptAlreadyRendered) return stripped;
const insertAt = boilerplateIndex !== -1 ? boilerplateIndex + 1 : lastAssistantToolUseIndex + 1;
const synthetic = createUserMessage({
content: viewedAgentTask.prompt,
timestamp: new Date(viewedAgentTask.startTime).toISOString(),
});
return [...stripped.slice(0, insertAt), synthetic, ...stripped.slice(insertAt)];
}, [viewedAgentTask, rawAgentMessages]);
const displayedMessages = viewedAgentTask
? (viewedAgentTask.messages ?? [])
? (displayedAgentMessages ?? [])
: usesSyncMessages
? messages
: deferredMessages;
@@ -6286,6 +6373,7 @@ export function REPL({
voiceInterimRange={voice.interimRange}
/>
<SessionBackgroundHint onBackgroundSession={handleBackgroundSession} isLoading={isLoading} />
<BackgroundAgentSelector />
</>
)}
{cursor && (