mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 17:47:27 +00:00
✨ 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:
parent
9d36cccd8e
commit
ab78b3832c
15 changed files with 306 additions and 97 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "已完成所有任务",
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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)}>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue