mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-16 03:52:17 +08:00
refactor: add primitives, plan mode display, and new session model selector
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import React, { memo, useMemo, useCallback } from 'react';
|
||||
|
||||
import type { Project } from '../../../types/app';
|
||||
import type { SubagentChildTool } from '../types/types';
|
||||
|
||||
import { getToolConfig } from './configs/toolConfigs';
|
||||
import { OneLineDisplay, CollapsibleDisplay, ToolDiffViewer, MarkdownContent, FileListContent, TodoListContent, TaskListContent, TextContent, QuestionAnswerContent, SubagentContainer } from './components';
|
||||
import { PlanDisplay } from './components/PlanDisplay';
|
||||
|
||||
type DiffLine = {
|
||||
type: string;
|
||||
@@ -119,6 +122,33 @@ export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
|
||||
);
|
||||
}
|
||||
|
||||
if (displayConfig.type === 'plan') {
|
||||
const title = typeof displayConfig.title === 'function'
|
||||
? displayConfig.title(parsedData)
|
||||
: displayConfig.title || 'Plan';
|
||||
|
||||
const contentProps = displayConfig.getContentProps?.(parsedData, {
|
||||
selectedProject,
|
||||
createDiff,
|
||||
onFileOpen
|
||||
}) || {};
|
||||
|
||||
const isStreaming = mode === 'input' && !toolResult;
|
||||
|
||||
return (
|
||||
<PlanDisplay
|
||||
title={title}
|
||||
content={contentProps.content || ''}
|
||||
defaultOpen={displayConfig.defaultOpen ?? autoExpandTools}
|
||||
isStreaming={isStreaming}
|
||||
showRawParameters={mode === 'input' && showRawParameters}
|
||||
rawContent={rawToolInput}
|
||||
toolName={toolName}
|
||||
toolId={toolId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (displayConfig.type === 'collapsible') {
|
||||
const title = typeof displayConfig.title === 'function'
|
||||
? displayConfig.title(parsedData)
|
||||
|
||||
135
src/components/chat/tools/components/PlanDisplay.tsx
Normal file
135
src/components/chat/tools/components/PlanDisplay.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import React from 'react';
|
||||
import { ChevronsUpDown, FileText } from 'lucide-react';
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
Button,
|
||||
Collapsible,
|
||||
CollapsibleTrigger,
|
||||
CollapsibleContent,
|
||||
Shimmer,
|
||||
} from '../../../../shared/view/ui';
|
||||
import { usePermission } from '../../../../contexts/PermissionContext';
|
||||
|
||||
import { MarkdownContent } from './ContentRenderers';
|
||||
|
||||
interface PlanDisplayProps {
|
||||
title: string;
|
||||
content: string;
|
||||
defaultOpen?: boolean;
|
||||
isStreaming?: boolean;
|
||||
showRawParameters?: boolean;
|
||||
rawContent?: string;
|
||||
toolName: string;
|
||||
toolId?: string;
|
||||
}
|
||||
|
||||
export const PlanDisplay: React.FC<PlanDisplayProps> = ({
|
||||
title,
|
||||
content,
|
||||
defaultOpen = false,
|
||||
isStreaming = false,
|
||||
showRawParameters = false,
|
||||
rawContent,
|
||||
toolName: _toolName,
|
||||
}) => {
|
||||
const permissionCtx = usePermission();
|
||||
|
||||
const pendingRequest = permissionCtx?.pendingPermissionRequests.find(
|
||||
(r) => r.toolName === 'ExitPlanMode' || r.toolName === 'exit_plan_mode'
|
||||
);
|
||||
|
||||
const handleBuild = () => {
|
||||
if (pendingRequest && permissionCtx) {
|
||||
permissionCtx.handlePermissionDecision(pendingRequest.requestId, { allow: true });
|
||||
}
|
||||
};
|
||||
|
||||
const handleRevise = () => {
|
||||
if (pendingRequest && permissionCtx) {
|
||||
permissionCtx.handlePermissionDecision(pendingRequest.requestId, {
|
||||
allow: false,
|
||||
message: 'User asked to revise the plan',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapsible defaultOpen={defaultOpen}>
|
||||
<Card className="my-1 flex flex-col shadow-none">
|
||||
{/* Header — always visible */}
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0 px-4 pb-0 pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<CardTitle className="text-sm font-semibold">
|
||||
{isStreaming ? <Shimmer>{title}</Shimmer> : title}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<CollapsibleTrigger className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground">
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle plan</span>
|
||||
</CollapsibleTrigger>
|
||||
</CardHeader>
|
||||
|
||||
{/* Collapsible content */}
|
||||
<CollapsibleContent>
|
||||
<CardContent className="px-4 pb-4 pt-3">
|
||||
{content ? (
|
||||
<MarkdownContent
|
||||
content={content}
|
||||
className="prose prose-sm max-w-none dark:prose-invert"
|
||||
/>
|
||||
) : isStreaming ? (
|
||||
<div className="py-2">
|
||||
<Shimmer>Generating plan...</Shimmer>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showRawParameters && rawContent && (
|
||||
<details className="group/raw relative mt-3">
|
||||
<summary className="flex cursor-pointer items-center gap-1.5 py-0.5 text-[11px] text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300">
|
||||
<svg
|
||||
className="h-2.5 w-2.5 transition-transform duration-150 group-open/raw:rotate-90"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
raw params
|
||||
</summary>
|
||||
<pre className="mt-1 overflow-hidden whitespace-pre-wrap break-words rounded border border-gray-200/40 bg-gray-50 p-2 font-mono text-[11px] text-gray-600 dark:border-gray-700/40 dark:bg-gray-900/50 dark:text-gray-400">
|
||||
{rawContent}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
|
||||
{/* Footer — always visible when permission is pending */}
|
||||
{pendingRequest && (
|
||||
<CardFooter className="justify-end gap-2 border-t border-border/40 px-4 pb-3 pt-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRevise}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
Revise
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleBuild}>
|
||||
Build{' '}
|
||||
<kbd className="ml-1 rounded bg-primary-foreground/20 px-1 py-0.5 font-mono text-[10px]">
|
||||
⌘↩
|
||||
</kbd>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
export interface ToolDisplayConfig {
|
||||
input: {
|
||||
type: 'one-line' | 'collapsible' | 'hidden';
|
||||
type: 'one-line' | 'collapsible' | 'plan' | 'hidden';
|
||||
// One-line config
|
||||
icon?: string;
|
||||
label?: string;
|
||||
@@ -31,7 +31,7 @@ export interface ToolDisplayConfig {
|
||||
result?: {
|
||||
hidden?: boolean;
|
||||
hideOnSuccess?: boolean;
|
||||
type?: 'one-line' | 'collapsible' | 'special';
|
||||
type?: 'one-line' | 'collapsible' | 'plan' | 'special';
|
||||
title?: string | ((result: any) => string);
|
||||
defaultOpen?: boolean;
|
||||
// Special result handlers
|
||||
@@ -494,7 +494,7 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
||||
|
||||
exit_plan_mode: {
|
||||
input: {
|
||||
type: 'collapsible',
|
||||
type: 'plan',
|
||||
title: 'Implementation plan',
|
||||
defaultOpen: true,
|
||||
contentType: 'markdown',
|
||||
@@ -503,29 +503,14 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
||||
})
|
||||
},
|
||||
result: {
|
||||
type: 'collapsible',
|
||||
contentType: 'markdown',
|
||||
getContentProps: (result) => {
|
||||
try {
|
||||
let parsed = result.content;
|
||||
if (typeof parsed === 'string') {
|
||||
parsed = JSON.parse(parsed);
|
||||
}
|
||||
return {
|
||||
content: parsed.plan?.replace(/\\n/g, '\n') || parsed.plan
|
||||
};
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse plan content:', e);
|
||||
return { content: '' };
|
||||
}
|
||||
}
|
||||
hidden: true
|
||||
}
|
||||
},
|
||||
|
||||
// Also register as ExitPlanMode (the actual tool name used by Claude)
|
||||
ExitPlanMode: {
|
||||
input: {
|
||||
type: 'collapsible',
|
||||
type: 'plan',
|
||||
title: 'Implementation plan',
|
||||
defaultOpen: true,
|
||||
contentType: 'markdown',
|
||||
@@ -534,22 +519,7 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
||||
})
|
||||
},
|
||||
result: {
|
||||
type: 'collapsible',
|
||||
contentType: 'markdown',
|
||||
getContentProps: (result) => {
|
||||
try {
|
||||
let parsed = result.content;
|
||||
if (typeof parsed === 'string') {
|
||||
parsed = JSON.parse(parsed);
|
||||
}
|
||||
return {
|
||||
content: parsed.plan?.replace(/\\n/g, '\n') || parsed.plan
|
||||
};
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse plan content:', e);
|
||||
return { content: '' };
|
||||
}
|
||||
}
|
||||
hidden: true
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user