feat(claude-code): CC subagent Thread rendering pipeline

Closes the viewing loop for CC subagent runs: the main-topic Agent tool
row now links into the spawned Thread, the Thread's Portal view renders
with provenance + read-only affordances, and the sidebar surfaces which
entries are subagent-produced.

UX:
- Agent render gains a trailing "View / Collapse full subagent
  conversation" toggle. It looks up the Thread by
  `metadata.sourceToolCallId === toolCallId` and calls
  openThreadInPortal / closeThreadPortal — hidden until the executor
  lazy-creates the Thread on the first subagent event, so it never
  renders as a no-op.
- Portal Thread Header shows a `[icon] subagentType` Tag next to the
  title ("Explore" / "General purpose" / ...). Inspector's folded row
  already exposes the same detail, so the icon + label stays
  consistent across the two surfaces.
- Portal Thread Chat flips into read-only mode when
  `metadata.sourceToolCallId` is set: ChatInput is hidden (the
  external CLI owns the session — new turns have nowhere to go),
  `disableEditing` propagates to every message (no double-click to
  edit, no user action bar), and `useThreadActionsBarConfig` wipes
  `bar` + `menu` across assistant / assistantGroup / user roles.
- Sidebar ThreadItem on both /agent and /group routes renders a plain
  "Subagent" badge next to the title when
  `metadata.subagentType` is present. The type detail deliberately
  lives on the Thread Header, not here — sidebar space is tight.

Shared resolver:
- `CC_SUBAGENT_TYPES` + `resolveCCSubagentType` move out of the
  Inspector into `packages/builtin-tool-claude-code/src/client/
  subagentTypes.ts` and re-export from the `/client` entry. Inspector
  + Portal Thread Header both consume it, so the icon/label stay in
  sync. Kept UI-level (LucideIcon | FC) rather than pushed into
  heterogeneous-agents, which is a pure-data package.
- Root package.json adds a direct dep on
  `@lobechat/builtin-tool-claude-code` so Portal Thread Header can
  import from `/client` (previously only transitive via builtin-tools).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arvin Xu 2026-04-21 15:36:08 +08:00
parent 9d36cccd8e
commit ab78b3832c
15 changed files with 306 additions and 97 deletions

View file

@ -400,7 +400,10 @@
"task.status.initializing": "Initializing task...",
"task.subtask": "Subtask",
"task.title": "Tasks",
"thread.closeSubagentThread": "Collapse subagent conversation",
"thread.divider": "Subtopic",
"thread.openSubagentThread": "View full subagent conversation",
"thread.subagentBadge": "Subagent",
"thread.threadMessageCount": "{{messageCount}} messages",
"thread.title": "Subtopic",
"todoProgress.allCompleted": "All tasks completed",

View file

@ -400,7 +400,10 @@
"task.status.initializing": "任务启动中…",
"task.subtask": "子任务",
"task.title": "任务",
"thread.closeSubagentThread": "收起子对话",
"thread.divider": "子话题",
"thread.openSubagentThread": "查看完整子对话",
"thread.subagentBadge": "子智能体",
"thread.threadMessageCount": "{{messageCount}} 条消息",
"thread.title": "子话题",
"todoProgress.allCompleted": "已完成所有任务",

View file

@ -211,6 +211,7 @@
"@lobechat/builtin-tool-agent-management": "workspace:*",
"@lobechat/builtin-tool-brief": "workspace:*",
"@lobechat/builtin-tool-calculator": "workspace:*",
"@lobechat/builtin-tool-claude-code": "workspace:*",
"@lobechat/builtin-tool-cloud-sandbox": "workspace:*",
"@lobechat/builtin-tool-creds": "workspace:*",
"@lobechat/builtin-tool-cron": "workspace:*",

View file

@ -4,25 +4,11 @@ import { inspectorTextStyles, shinyTextStyles } from '@lobechat/shared-tool-ui/s
import type { BuiltinInspectorProps } from '@lobechat/types';
import { GroupBotIcon } from '@lobehub/ui/icons';
import { createStaticStyles, cx } from 'antd-style';
import { Compass, type LucideIcon, Search, Settings } from 'lucide-react';
import { type ComponentType, memo } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { type AgentArgs, ClaudeCodeApiName } from '../../types';
type IconComponent = LucideIcon | ComponentType<{ className?: string; size?: number }>;
/**
* Known subagent templates shipped with CC. `subagent_type` on the tool call
* is matched against this map; unknown values fall back to `GroupBotIcon` and
* the raw type string, so user-defined subagents still render sensibly.
*/
const SUBAGENT_TYPES: Record<string, { icon: IconComponent; label: string }> = {
'Explore': { icon: Search, label: 'Explore' },
'Plan': { icon: Compass, label: 'Plan' },
'general-purpose': { icon: GroupBotIcon, label: 'General purpose' },
'statusline-setup': { icon: Settings, label: 'Statusline setup' },
};
import { resolveCCSubagentType } from '../subagentTypes';
const styles = createStaticStyles(({ css, cssVar }) => ({
chip: css`
@ -78,9 +64,9 @@ export const AgentInspector = memo<BuiltinInspectorProps<AgentArgs>>(
const isShiny = isArgumentsStreaming || isLoading;
const known = subagentType ? SUBAGENT_TYPES[subagentType] : undefined;
const Icon = known?.icon ?? GroupBotIcon;
const labelText = known?.label ?? subagentType ?? fallbackLabel;
const resolved = resolveCCSubagentType(subagentType);
const Icon = resolved?.icon ?? GroupBotIcon;
const labelText = resolved?.label ?? fallbackLabel;
return (
<div className={cx(inspectorTextStyles.root, isShiny && shinyTextStyles.shinyText)}>

View file

@ -1,11 +1,15 @@
'use client';
import type { BuiltinRenderProps } from '@lobechat/types';
import { Flexbox, Markdown, Text } from '@lobehub/ui';
import { Button, Flexbox, Markdown, Text } from '@lobehub/ui';
import { createStaticStyles } from 'antd-style';
import { memo } from 'react';
import { ListTree } from 'lucide-react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useChatStore } from '@/store/chat';
import { portalThreadSelectors, threadSelectors } from '@/store/chat/selectors';
import type { AgentArgs } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
@ -13,11 +17,18 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
padding-block: 4px;
`,
label: css`
margin-block-end: 4px;
padding-inline-start: 4px;
font-size: 12px;
color: ${cssVar.colorTextTertiary};
`,
labelRow: css`
margin-block-end: 4px;
`,
openThread: css`
height: 22px;
padding-inline: 6px;
font-size: 12px;
`,
promptBox: css`
padding-block: 8px;
padding-inline: 12px;
@ -43,44 +54,97 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
* since the two Markdown bubbles are otherwise indistinguishable once the
* subagent's reply happens to use the same tone as the prompt.
*
* Note: subagent internal turns are persisted as a separate Thread (linked
* via `metadata.sourceToolCallId`) by the executor this render does NOT
* replay those; it only surfaces the request/response pair that belongs to
* THIS tool call.
* Each subagent spawn also persists as a Thread linked by
* `metadata.sourceToolCallId = toolCallId`; when that Thread exists, the
* result-label row exposes a toggle to open / collapse it in the portal
* (clicking again while already open closes it rather than acting as a
* dead-end disabled state). The executor creates the Thread lazily on the
* first subagent event, so the lookup can briefly return `undefined` the
* button is hidden in that window instead of faked into a no-op.
*/
const Agent = memo<BuiltinRenderProps<AgentArgs, unknown, string>>(({ args, content }) => {
const { t } = useTranslation('plugin');
const prompt = args?.prompt?.trim();
const result = typeof content === 'string' ? content.trim() : '';
const Agent = memo<BuiltinRenderProps<AgentArgs, unknown, string>>(
({ args, content, toolCallId }) => {
const { t } = useTranslation('plugin');
const { t: tChat } = useTranslation('chat');
const prompt = args?.prompt?.trim();
const result = typeof content === 'string' ? content.trim() : '';
if (!prompt && !result) return null;
const subagentThread = useChatStore((s) =>
toolCallId
? (threadSelectors.currentTopicThreads(s) ?? []).find(
(thread) => thread.metadata?.sourceToolCallId === toolCallId,
)
: undefined,
);
const openThreadInPortal = useChatStore((s) => s.openThreadInPortal);
const closeThreadPortal = useChatStore((s) => s.closeThreadPortal);
const portalThreadId = useChatStore(portalThreadSelectors.portalThreadId);
const isOpenInPortal = !!subagentThread && portalThreadId === subagentThread.id;
return (
<Flexbox className={styles.container} gap={12}>
{prompt && (
<Flexbox>
<Text className={styles.label}>{t('builtins.lobe-claude-code.agent.instruction')}</Text>
<Flexbox className={styles.promptBox}>
<Markdown style={{ maxHeight: 240, overflow: 'auto' }} variant={'chat'}>
{prompt}
</Markdown>
const handleToggleThread = useCallback(() => {
if (!subagentThread) return;
if (isOpenInPortal) {
closeThreadPortal();
} else {
openThreadInPortal(subagentThread.id, subagentThread.sourceMessageId);
}
}, [subagentThread, isOpenInPortal, openThreadInPortal, closeThreadPortal]);
if (!prompt && !result && !subagentThread) return null;
const showResultSection = !!result || !!subagentThread;
return (
<Flexbox className={styles.container} gap={12}>
{prompt && (
<Flexbox>
<Text className={styles.label} style={{ marginBlockEnd: 4 }}>
{t('builtins.lobe-claude-code.agent.instruction')}
</Text>
<Flexbox className={styles.promptBox}>
<Markdown style={{ maxHeight: 240, overflow: 'auto' }} variant={'chat'}>
{prompt}
</Markdown>
</Flexbox>
</Flexbox>
</Flexbox>
)}
)}
{result && (
<Flexbox>
<Text className={styles.label}>{t('builtins.lobe-claude-code.agent.result')}</Text>
<Flexbox className={styles.resultBox}>
<Markdown style={{ maxHeight: 320, overflow: 'auto' }} variant={'chat'}>
{result}
</Markdown>
{showResultSection && (
<Flexbox>
<Flexbox
horizontal
align={'center'}
className={styles.labelRow}
justify={'space-between'}
>
<Text className={styles.label}>{t('builtins.lobe-claude-code.agent.result')}</Text>
{subagentThread && (
<Button
className={styles.openThread}
icon={ListTree}
size={'small'}
type={'text'}
onClick={handleToggleThread}
>
{isOpenInPortal
? tChat('thread.closeSubagentThread')
: tChat('thread.openSubagentThread')}
</Button>
)}
</Flexbox>
{result && (
<Flexbox className={styles.resultBox}>
<Markdown style={{ maxHeight: 320, overflow: 'auto' }} variant={'chat'}>
{result}
</Markdown>
</Flexbox>
)}
</Flexbox>
</Flexbox>
)}
</Flexbox>
);
});
)}
</Flexbox>
);
},
);
Agent.displayName = 'ClaudeCodeAgent';

View file

@ -1,3 +1,4 @@
export { ClaudeCodeApiName, ClaudeCodeIdentifier } from '../types';
export { ClaudeCodeInspectors } from './Inspector';
export { ClaudeCodeRenderDisplayControls, ClaudeCodeRenders } from './Render';
export { CC_SUBAGENT_TYPES, type CCSubagentTypeInfo, resolveCCSubagentType } from './subagentTypes';

View file

@ -0,0 +1,40 @@
import { GroupBotIcon } from '@lobehub/ui/icons';
import { Compass, type LucideIcon, Search, Settings } from 'lucide-react';
import type { FC } from 'react';
type IconComponent = LucideIcon | FC<{ className?: string; size?: number }>;
export interface CCSubagentTypeInfo {
icon: IconComponent;
label: string;
}
/**
* Known subagent templates shipped with CC. `subagent_type` on the Agent
* tool call is matched against this map; unknown values fall through to
* the raw string plus a generic bot icon so user-defined subagents still
* render sensibly.
*
* UI-level (icons are React components) so it lives in the CC client entry
* rather than `@lobechat/heterogeneous-agents` that package stays a
* pure-data home for adapter orchestration.
*/
export const CC_SUBAGENT_TYPES: Record<string, CCSubagentTypeInfo> = {
'Explore': { icon: Search, label: 'Explore' },
'Plan': { icon: Compass, label: 'Plan' },
'general-purpose': { icon: GroupBotIcon, label: 'General purpose' },
'statusline-setup': { icon: Settings, label: 'Statusline setup' },
};
/**
* Resolve a `subagent_type` string to `{ icon, label }`. Returns `undefined`
* when the input is empty/whitespace so callers can distinguish "no value"
* from "unknown value" the latter gets a synthesized fallback.
*/
export const resolveCCSubagentType = (
subagentType: string | undefined | null,
): CCSubagentTypeInfo | undefined => {
const trimmed = subagentType?.trim();
if (!trimmed) return undefined;
return CC_SUBAGENT_TYPES[trimmed] ?? { icon: GroupBotIcon, label: trimmed };
};

View file

@ -15,7 +15,7 @@ import {
import SkeletonList from '@/features/Conversation/components/SkeletonList';
import { useOperationState } from '@/hooks/useOperationState';
import { useChatStore } from '@/store/chat';
import { threadSelectors } from '@/store/chat/selectors';
import { portalThreadSelectors, threadSelectors } from '@/store/chat/selectors';
import { type MessageMapKeyInput } from '@/store/chat/utils/messageMapKey';
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
@ -26,7 +26,11 @@ import { useThreadActionsBarConfig } from './useThreadActionsBarConfig';
* Inner component that uses ConversationStore for message rendering
* Must be inside ConversationProvider to access the store
*/
const ThreadChatContent = memo(() => {
interface ThreadChatContentProps {
isSubagentThread: boolean;
}
const ThreadChatContent = memo<ThreadChatContentProps>(({ isSubagentThread }) => {
// Get display messages from ConversationStore to determine thread divider position
// With the new backend API, parent messages have threadId === null
// and thread messages have threadId === context.threadId
@ -62,14 +66,14 @@ const ThreadChatContent = memo(() => {
return (
<MessageItem
inPortalThread
disableEditing={isParentMessage}
disableEditing={isSubagentThread || isParentMessage}
endRender={enableThreadDivider ? <ThreadDivider /> : undefined}
id={id}
index={index}
/>
);
},
[threadSourceInfo.sourceMessageId, threadSourceInfo.sourceMessageIndex],
[threadSourceInfo.sourceMessageId, threadSourceInfo.sourceMessageIndex, isSubagentThread],
);
return (
@ -93,7 +97,7 @@ const ThreadChatContent = memo(() => {
<ChatList itemContent={itemContent} />
</Flexbox>
</Suspense>
<ChatInput leftActions={['typo', 'stt', 'portalToken']} />
{!isSubagentThread && <ChatInput leftActions={['typo', 'stt', 'portalToken']} />}
</>
);
});
@ -118,8 +122,18 @@ const ThreadChat = memo(() => {
s.newThreadMode,
]);
// Subagent threads are auto-spawned by a parent tool call (CC's `Agent`
// tool etc.); the external CLI owns the session so the user can't inject
// new turns or mutate existing ones. `sourceToolCallId` is set by the
// executor on every spawn — unambiguous marker to flip the thread into a
// read-only record (hides composer, wipes per-message actions, disables
// double-click editing).
const isSubagentThread = useChatStore(
(s) => !!portalThreadSelectors.portalCurrentThread(s)?.metadata?.sourceToolCallId,
);
// Get thread-specific actionsBar config
const actionsBarConfig = useThreadActionsBarConfig();
const actionsBarConfig = useThreadActionsBarConfig({ readonly: isSubagentThread });
// Build ConversationContext for thread
// When creating new thread (!portalThreadId), use isNew + scope: 'thread'
@ -210,7 +224,7 @@ const ThreadChat = memo(() => {
replaceMessages(msgs, { context: ctx });
}}
>
<ThreadChatContent />
<ThreadChatContent isSubagentThread={isSubagentThread} />
</ConversationProvider>
);
});

View file

@ -2,7 +2,21 @@
import { useMemo } from 'react';
import { type ActionsBarConfig } from '@/features/Conversation';
import { type ActionsBarConfig, type MessageActionsConfig } from '@/features/Conversation';
interface UseThreadActionsBarConfigOptions {
/**
* When true, every role's bar+menu is wiped so the thread renders as a
* read-only record. Used for subagent Threads whose contents are owned
* by the external CLI edit / regenerate / delete would mutate state
* the user can't meaningfully re-drive.
*/
readonly?: boolean;
}
// Empty arrays (not `undefined`) so the underlying `actionsConfig?.bar ?? DEFAULT_BAR`
// fallback doesn't fill them back in with the default set.
const EMPTY_MESSAGE_ACTIONS: MessageActionsConfig = { bar: [], menu: [] };
/**
* Hook to create thread-specific actionsBar configuration
@ -10,19 +24,27 @@ import { type ActionsBarConfig } from '@/features/Conversation';
* In thread mode:
* - Parent messages (before thread divider) should be read-only (handled via disableEditing prop)
* - Thread messages can be edited/deleted normally
* - Subagent threads (`readonly: true`) hide the whole action bar across roles
*
* Note: Parent message disabling is now handled directly in the itemContent renderer
* via the disableEditing prop, rather than through actionsBar config.
*/
export const useThreadActionsBarConfig = (): ActionsBarConfig => {
// For thread mode, we return a minimal config
// Parent message editing is handled via disableEditing prop in itemContent
export const useThreadActionsBarConfig = ({
readonly = false,
}: UseThreadActionsBarConfigOptions = {}): ActionsBarConfig => {
return useMemo(
() => ({
// Thread-specific actions can be added here if needed
assistant: {},
user: {},
}),
[],
() =>
readonly
? {
assistant: EMPTY_MESSAGE_ACTIONS,
assistantGroup: EMPTY_MESSAGE_ACTIONS,
user: EMPTY_MESSAGE_ACTIONS,
}
: {
// Thread-specific actions can be added here if needed
assistant: {},
user: {},
},
[readonly],
);
};

View file

@ -1,4 +1,5 @@
import { Flexbox, Icon, Text } from '@lobehub/ui';
import { resolveCCSubagentType } from '@lobechat/builtin-tool-claude-code/client';
import { Flexbox, Icon, Tag, Text } from '@lobehub/ui';
import { cssVar } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { ListTree } from 'lucide-react';
@ -13,25 +14,43 @@ import { oneLineEllipsis } from '@/styles';
const Active = memo(() => {
const currentThread = useChatStore(portalThreadSelectors.portalCurrentThread, isEqual);
if (!currentThread) return null;
// Subagent spawn → show the specific template (e.g. "Explore",
// "General purpose") as a chip next to the title. Sidebar only marks
// "Subagent" generically; the header is where the detail belongs.
const subagentTypeInfo = resolveCCSubagentType(currentThread.metadata?.subagentType);
return (
currentThread && (
<Flexbox horizontal align={'center'} gap={8} style={{ marginInlineStart: 4 }}>
<Icon color={cssVar.colorTextSecondary} icon={ListTree} size={18} />
<Text
className={oneLineEllipsis}
ellipsis={true}
style={{ color: cssVar.colorTextSecondary, fontSize: 14 }}
<Flexbox horizontal align={'center'} gap={8} style={{ marginInlineStart: 4 }}>
<Icon color={cssVar.colorTextSecondary} icon={ListTree} size={18} />
<Text
className={oneLineEllipsis}
ellipsis={true}
style={{ color: cssVar.colorTextSecondary, fontSize: 14 }}
>
{currentThread.title === LOADING_FLAT ? (
<Flexbox flex={1} height={30} justify={'center'}>
<BubblesLoading />
</Flexbox>
) : (
currentThread.title
)}
</Text>
{subagentTypeInfo && (
<Tag
icon={<Icon icon={subagentTypeInfo.icon} />}
size={'small'}
style={{
color: cssVar.colorTextDescription,
flexShrink: 0,
fontSize: 11,
}}
>
{currentThread?.title === LOADING_FLAT ? (
<Flexbox flex={1} height={30} justify={'center'}>
<BubblesLoading />
</Flexbox>
) : (
currentThread?.title
)}
</Text>
</Flexbox>
)
{subagentTypeInfo.label}
</Tag>
)}
</Flexbox>
);
});

View file

@ -437,7 +437,10 @@ export default {
'task.status.fetchingDetails': 'Fetching details...',
'task.status.initializing': 'Initializing task...',
'task.subtask': 'Subtask',
'thread.closeSubagentThread': 'Collapse subagent conversation',
'thread.divider': 'Subtopic',
'thread.openSubagentThread': 'View full subagent conversation',
'thread.subagentBadge': 'Subagent',
'thread.threadMessageCount': '{{messageCount}} messages',
'thread.title': 'Subtopic',
'todoProgress.allCompleted': 'All tasks completed',

View file

@ -1,7 +1,8 @@
import { Icon } from '@lobehub/ui';
import { Flexbox, Icon, Tag } from '@lobehub/ui';
import { TreeDownRightIcon } from '@lobehub/ui/icons';
import { cssVar } from 'antd-style';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import NavItem from '@/features/NavPanel/components/NavItem';
import { useChatStore } from '@/store/chat';
@ -14,10 +15,12 @@ import { useThreadItemDropdownMenu } from './useDropdownMenu';
export interface ThreadItemProps {
id: string;
index: number;
isSubagent?: boolean;
title: string;
}
const ThreadItem = memo<ThreadItemProps>(({ title, id }) => {
const ThreadItem = memo<ThreadItemProps>(({ title, id, isSubagent }) => {
const { t } = useTranslation('chat');
const [editing, activeThreadId] = useChatStore((s) => [
s.threadRenamingId === id,
s.activeThreadId,
@ -44,6 +47,9 @@ const ThreadItem = memo<ThreadItemProps>(({ title, id }) => {
const active = id === activeThreadId;
// Subagent threads (spawned by an external agent's subagent tool call)
// only get a plain "Subagent" badge — the specific template name is
// surfaced on the Thread header instead, where there's room for it.
return (
<>
<NavItem
@ -52,7 +58,23 @@ const ThreadItem = memo<ThreadItemProps>(({ title, id }) => {
contextMenuItems={dropdownMenu}
disabled={editing}
icon={<Icon color={cssVar.colorTextDescription} icon={TreeDownRightIcon} size={'small'} />}
title={title}
title={
isSubagent ? (
<Flexbox horizontal align={'center'} flex={1} gap={6}>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{title}
</span>
<Tag
size={'small'}
style={{ color: cssVar.colorTextDescription, flexShrink: 0, fontSize: 10 }}
>
{t('thread.subagentBadge')}
</Tag>
</Flexbox>
) : (
title
)
}
onClick={handleClick}
/>
<Editing id={id} title={title} toggleEditing={toggleEditing} />

View file

@ -18,7 +18,13 @@ const ThreadList = memo(() => {
return (
<Flexbox gap={1} paddingBlock={1}>
{threads?.map((item, index) => (
<ThreadItem id={item.id} index={index} key={item.id} title={item.title} />
<ThreadItem
id={item.id}
index={index}
isSubagent={!!item.metadata?.subagentType}
key={item.id}
title={item.title}
/>
))}
</Flexbox>
);

View file

@ -1,7 +1,8 @@
import { Icon } from '@lobehub/ui';
import { Flexbox, Icon, Tag } from '@lobehub/ui';
import { TreeDownRightIcon } from '@lobehub/ui/icons';
import { cssVar } from 'antd-style';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import NavItem from '@/features/NavPanel/components/NavItem';
import { useChatStore } from '@/store/chat';
@ -14,10 +15,12 @@ import { useThreadItemDropdownMenu } from './useDropdownMenu';
export interface ThreadItemProps {
id: string;
index: number;
isSubagent?: boolean;
title: string;
}
const ThreadItem = memo<ThreadItemProps>(({ title, id }) => {
const ThreadItem = memo<ThreadItemProps>(({ title, id, isSubagent }) => {
const { t } = useTranslation('chat');
const [editing, activeThreadId] = useChatStore((s) => [
s.threadRenamingId === id,
s.activeThreadId,
@ -52,7 +55,23 @@ const ThreadItem = memo<ThreadItemProps>(({ title, id }) => {
contextMenuItems={dropdownMenu}
disabled={editing}
icon={<Icon color={cssVar.colorTextDescription} icon={TreeDownRightIcon} size={'small'} />}
title={title}
title={
isSubagent ? (
<Flexbox horizontal align={'center'} flex={1} gap={6}>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{title}
</span>
<Tag
size={'small'}
style={{ color: cssVar.colorTextDescription, flexShrink: 0, fontSize: 10 }}
>
{t('thread.subagentBadge')}
</Tag>
</Flexbox>
) : (
title
)
}
onClick={handleClick}
/>
<Editing id={id} title={title} toggleEditing={toggleEditing} />

View file

@ -18,7 +18,13 @@ const ThreadList = memo(() => {
return (
<Flexbox gap={1} paddingBlock={1}>
{threads?.map((item, index) => (
<ThreadItem id={item.id} index={index} key={item.id} title={item.title} />
<ThreadItem
id={item.id}
index={index}
isSubagent={!!item.metadata?.subagentType}
key={item.id}
title={item.title}
/>
))}
</Flexbox>
);