@codrstudio/openclaude-chat 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/components/StreamingIndicator.js +5 -5
  2. package/dist/display/DisplayReactRenderer.js +12 -12
  3. package/dist/display/react-sandbox/bootstrap.js +150 -150
  4. package/dist/styles.css +1 -2
  5. package/package.json +64 -61
  6. package/src/components/Chat.tsx +107 -107
  7. package/src/components/ErrorNote.tsx +35 -35
  8. package/src/components/LazyRender.tsx +42 -42
  9. package/src/components/Markdown.tsx +114 -114
  10. package/src/components/MessageBubble.tsx +107 -107
  11. package/src/components/MessageInput.tsx +421 -421
  12. package/src/components/MessageList.tsx +153 -153
  13. package/src/components/StreamingIndicator.tsx +19 -19
  14. package/src/display/AlertRenderer.tsx +23 -23
  15. package/src/display/CarouselRenderer.tsx +141 -141
  16. package/src/display/ChartRenderer.tsx +195 -195
  17. package/src/display/ChoiceButtonsRenderer.tsx +114 -114
  18. package/src/display/CodeBlockRenderer.tsx +49 -49
  19. package/src/display/ComparisonTableRenderer.tsx +132 -132
  20. package/src/display/DataTableRenderer.tsx +144 -144
  21. package/src/display/DisplayReactRenderer.tsx +269 -269
  22. package/src/display/FileCardRenderer.tsx +55 -55
  23. package/src/display/GalleryRenderer.tsx +65 -65
  24. package/src/display/ImageViewerRenderer.tsx +114 -114
  25. package/src/display/LinkPreviewRenderer.tsx +74 -74
  26. package/src/display/MapViewRenderer.tsx +75 -75
  27. package/src/display/MetricCardRenderer.tsx +29 -29
  28. package/src/display/PriceHighlightRenderer.tsx +62 -62
  29. package/src/display/ProductCardRenderer.tsx +112 -112
  30. package/src/display/ProgressStepsRenderer.tsx +59 -59
  31. package/src/display/SourcesListRenderer.tsx +47 -47
  32. package/src/display/SpreadsheetRenderer.tsx +86 -86
  33. package/src/display/StepTimelineRenderer.tsx +75 -75
  34. package/src/display/index.ts +21 -21
  35. package/src/display/react-sandbox/bootstrap.ts +155 -155
  36. package/src/display/registry.ts +84 -84
  37. package/src/display/sdk-types.ts +217 -217
  38. package/src/hooks/ChatProvider.tsx +21 -21
  39. package/src/hooks/useIsMobile.ts +15 -15
  40. package/src/hooks/useOpenClaudeChat.ts +476 -476
  41. package/src/index.ts +76 -76
  42. package/src/lib/utils.ts +6 -6
  43. package/src/parts/PartErrorBoundary.tsx +51 -51
  44. package/src/parts/PartRenderer.tsx +145 -145
  45. package/src/parts/ReasoningBlock.tsx +41 -41
  46. package/src/parts/ToolActivity.tsx +78 -78
  47. package/src/parts/ToolResult.tsx +79 -79
  48. package/src/styles.css +2 -2
  49. package/src/types.ts +41 -41
  50. package/src/ui/alert.tsx +77 -77
  51. package/src/ui/badge.tsx +36 -36
  52. package/src/ui/button.tsx +54 -54
  53. package/src/ui/card.tsx +68 -68
  54. package/src/ui/collapsible.tsx +7 -7
  55. package/src/ui/dialog.tsx +122 -122
  56. package/src/ui/dropdown-menu.tsx +76 -76
  57. package/src/ui/input.tsx +24 -24
  58. package/src/ui/progress.tsx +36 -36
  59. package/src/ui/scroll-area.tsx +48 -48
  60. package/src/ui/separator.tsx +31 -31
  61. package/src/ui/skeleton.tsx +9 -9
  62. package/src/ui/table.tsx +114 -114
package/src/index.ts CHANGED
@@ -1,76 +1,76 @@
1
- // @codrstudio/openclaude-chat — barrel
2
-
3
- // Tipos publicos
4
- export type {
5
- Message,
6
- MessagePart,
7
- MessageRole,
8
- TextPart,
9
- ReasoningPart,
10
- ToolInvocationPart,
11
- ToolInvocationState,
12
- } from "./types.js";
13
-
14
- // Componente principal
15
- export { Chat } from "./components/Chat.js";
16
- export type { ChatProps } from "./components/Chat.js";
17
-
18
- // Hook + Provider
19
- export { useOpenClaudeChat } from "./hooks/useOpenClaudeChat.js";
20
- export type {
21
- UseOpenClaudeChatOptions,
22
- UseOpenClaudeChatReturn,
23
- } from "./hooks/useOpenClaudeChat.js";
24
-
25
- export { ChatProvider, useChatContext } from "./hooks/ChatProvider.js";
26
- export type { ChatProviderProps } from "./hooks/ChatProvider.js";
27
-
28
- // Subcomponentes
29
- export { Markdown } from "./components/Markdown.js";
30
- export { StreamingIndicator } from "./components/StreamingIndicator.js";
31
- export { ErrorNote } from "./components/ErrorNote.js";
32
- export type { ErrorNoteProps } from "./components/ErrorNote.js";
33
- export { MessageBubble } from "./components/MessageBubble.js";
34
- export type { MessageBubbleProps } from "./components/MessageBubble.js";
35
- export { MessageList } from "./components/MessageList.js";
36
- export type { MessageListProps } from "./components/MessageList.js";
37
- export { MessageInput } from "./components/MessageInput.js";
38
- export type { MessageInputProps, Attachment } from "./components/MessageInput.js";
39
-
40
- // Parts
41
- export { PartRenderer } from "./parts/PartRenderer.js";
42
- export type { PartRendererProps } from "./parts/PartRenderer.js";
43
- export { ReasoningBlock } from "./parts/ReasoningBlock.js";
44
- export type { ReasoningBlockProps } from "./parts/ReasoningBlock.js";
45
- export { ToolActivity, defaultToolIconMap } from "./parts/ToolActivity.js";
46
- export type { ToolActivityProps, ToolActivityState } from "./parts/ToolActivity.js";
47
- export { ToolResult } from "./parts/ToolResult.js";
48
- export type { ToolResultProps } from "./parts/ToolResult.js";
49
-
50
- // Display renderers
51
- export { AlertRenderer } from "./display/AlertRenderer.js";
52
- export { MetricCardRenderer } from "./display/MetricCardRenderer.js";
53
- export { PriceHighlightRenderer } from "./display/PriceHighlightRenderer.js";
54
- export { FileCardRenderer } from "./display/FileCardRenderer.js";
55
- export { CodeBlockRenderer } from "./display/CodeBlockRenderer.js";
56
- export { SourcesListRenderer } from "./display/SourcesListRenderer.js";
57
- export { StepTimelineRenderer } from "./display/StepTimelineRenderer.js";
58
- export { ProgressStepsRenderer } from "./display/ProgressStepsRenderer.js";
59
- export { ChartRenderer } from "./display/ChartRenderer.js";
60
- export { CarouselRenderer } from "./display/CarouselRenderer.js";
61
- export { ProductCardRenderer } from "./display/ProductCardRenderer.js";
62
- export { ComparisonTableRenderer } from "./display/ComparisonTableRenderer.js";
63
- export { DataTableRenderer } from "./display/DataTableRenderer.js";
64
- export { SpreadsheetRenderer } from "./display/SpreadsheetRenderer.js";
65
- export { GalleryRenderer } from "./display/GalleryRenderer.js";
66
- export { ImageViewerRenderer } from "./display/ImageViewerRenderer.js";
67
- export { LinkPreviewRenderer } from "./display/LinkPreviewRenderer.js";
68
- export { MapViewRenderer } from "./display/MapViewRenderer.js";
69
- export { ChoiceButtonsRenderer } from "./display/ChoiceButtonsRenderer.js";
70
-
71
- // Registry
72
- export { defaultDisplayRenderers, resolveDisplayRenderer } from "./display/registry.js";
73
- export type { DisplayRendererMap, DisplayActionName } from "./display/registry.js";
74
-
75
- // useIsMobile helper reuse
76
- export { useIsMobile } from "./hooks/useIsMobile.js";
1
+ // @codrstudio/openclaude-chat — barrel
2
+
3
+ // Tipos publicos
4
+ export type {
5
+ Message,
6
+ MessagePart,
7
+ MessageRole,
8
+ TextPart,
9
+ ReasoningPart,
10
+ ToolInvocationPart,
11
+ ToolInvocationState,
12
+ } from "./types.js";
13
+
14
+ // Componente principal
15
+ export { Chat } from "./components/Chat.js";
16
+ export type { ChatProps } from "./components/Chat.js";
17
+
18
+ // Hook + Provider
19
+ export { useOpenClaudeChat } from "./hooks/useOpenClaudeChat.js";
20
+ export type {
21
+ UseOpenClaudeChatOptions,
22
+ UseOpenClaudeChatReturn,
23
+ } from "./hooks/useOpenClaudeChat.js";
24
+
25
+ export { ChatProvider, useChatContext } from "./hooks/ChatProvider.js";
26
+ export type { ChatProviderProps } from "./hooks/ChatProvider.js";
27
+
28
+ // Subcomponentes
29
+ export { Markdown } from "./components/Markdown.js";
30
+ export { StreamingIndicator } from "./components/StreamingIndicator.js";
31
+ export { ErrorNote } from "./components/ErrorNote.js";
32
+ export type { ErrorNoteProps } from "./components/ErrorNote.js";
33
+ export { MessageBubble } from "./components/MessageBubble.js";
34
+ export type { MessageBubbleProps } from "./components/MessageBubble.js";
35
+ export { MessageList } from "./components/MessageList.js";
36
+ export type { MessageListProps } from "./components/MessageList.js";
37
+ export { MessageInput } from "./components/MessageInput.js";
38
+ export type { MessageInputProps, Attachment } from "./components/MessageInput.js";
39
+
40
+ // Parts
41
+ export { PartRenderer } from "./parts/PartRenderer.js";
42
+ export type { PartRendererProps } from "./parts/PartRenderer.js";
43
+ export { ReasoningBlock } from "./parts/ReasoningBlock.js";
44
+ export type { ReasoningBlockProps } from "./parts/ReasoningBlock.js";
45
+ export { ToolActivity, defaultToolIconMap } from "./parts/ToolActivity.js";
46
+ export type { ToolActivityProps, ToolActivityState } from "./parts/ToolActivity.js";
47
+ export { ToolResult } from "./parts/ToolResult.js";
48
+ export type { ToolResultProps } from "./parts/ToolResult.js";
49
+
50
+ // Display renderers
51
+ export { AlertRenderer } from "./display/AlertRenderer.js";
52
+ export { MetricCardRenderer } from "./display/MetricCardRenderer.js";
53
+ export { PriceHighlightRenderer } from "./display/PriceHighlightRenderer.js";
54
+ export { FileCardRenderer } from "./display/FileCardRenderer.js";
55
+ export { CodeBlockRenderer } from "./display/CodeBlockRenderer.js";
56
+ export { SourcesListRenderer } from "./display/SourcesListRenderer.js";
57
+ export { StepTimelineRenderer } from "./display/StepTimelineRenderer.js";
58
+ export { ProgressStepsRenderer } from "./display/ProgressStepsRenderer.js";
59
+ export { ChartRenderer } from "./display/ChartRenderer.js";
60
+ export { CarouselRenderer } from "./display/CarouselRenderer.js";
61
+ export { ProductCardRenderer } from "./display/ProductCardRenderer.js";
62
+ export { ComparisonTableRenderer } from "./display/ComparisonTableRenderer.js";
63
+ export { DataTableRenderer } from "./display/DataTableRenderer.js";
64
+ export { SpreadsheetRenderer } from "./display/SpreadsheetRenderer.js";
65
+ export { GalleryRenderer } from "./display/GalleryRenderer.js";
66
+ export { ImageViewerRenderer } from "./display/ImageViewerRenderer.js";
67
+ export { LinkPreviewRenderer } from "./display/LinkPreviewRenderer.js";
68
+ export { MapViewRenderer } from "./display/MapViewRenderer.js";
69
+ export { ChoiceButtonsRenderer } from "./display/ChoiceButtonsRenderer.js";
70
+
71
+ // Registry
72
+ export { defaultDisplayRenderers, resolveDisplayRenderer } from "./display/registry.js";
73
+ export type { DisplayRendererMap, DisplayActionName } from "./display/registry.js";
74
+
75
+ // useIsMobile helper reuse
76
+ export { useIsMobile } from "./hooks/useIsMobile.js";
package/src/lib/utils.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { clsx, type ClassValue } from "clsx";
2
- import { twMerge } from "tailwind-merge";
3
-
4
- export function cn(...inputs: ClassValue[]) {
5
- return twMerge(clsx(inputs));
6
- }
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -1,51 +1,51 @@
1
- import React from "react";
2
- import { AlertTriangle } from "lucide-react";
3
-
4
- interface State {
5
- hasError: boolean;
6
- message?: string;
7
- }
8
-
9
- interface Props {
10
- children: React.ReactNode;
11
- label?: string;
12
- }
13
-
14
- /**
15
- * Isola o crash de um renderer (ex: display widget com input invalido) pra
16
- * evitar que um unico bloco derrube a arvore React inteira. O chat continua
17
- * renderizando os outros parts, e no lugar do part quebrado mostra um aviso.
18
- */
19
- export class PartErrorBoundary extends React.Component<Props, State> {
20
- state: State = { hasError: false };
21
-
22
- static getDerivedStateFromError(error: unknown): State {
23
- return {
24
- hasError: true,
25
- message: error instanceof Error ? error.message : String(error),
26
- };
27
- }
28
-
29
- componentDidCatch(error: unknown, info: unknown) {
30
- // eslint-disable-next-line no-console
31
- console.warn("[openclaude-chat] part renderer failed:", error, info);
32
- }
33
-
34
- render() {
35
- if (this.state.hasError) {
36
- return (
37
- <div
38
- role="alert"
39
- className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive"
40
- >
41
- <AlertTriangle className="size-3.5 shrink-0 mt-0.5" />
42
- <span className="flex-1 min-w-0 break-words font-mono">
43
- {this.props.label ? `[${this.props.label}] ` : ""}
44
- {this.state.message ?? "Falha ao renderizar bloco"}
45
- </span>
46
- </div>
47
- );
48
- }
49
- return this.props.children;
50
- }
51
- }
1
+ import React from "react";
2
+ import { AlertTriangle } from "lucide-react";
3
+
4
+ interface State {
5
+ hasError: boolean;
6
+ message?: string;
7
+ }
8
+
9
+ interface Props {
10
+ children: React.ReactNode;
11
+ label?: string;
12
+ }
13
+
14
+ /**
15
+ * Isola o crash de um renderer (ex: display widget com input invalido) pra
16
+ * evitar que um unico bloco derrube a arvore React inteira. O chat continua
17
+ * renderizando os outros parts, e no lugar do part quebrado mostra um aviso.
18
+ */
19
+ export class PartErrorBoundary extends React.Component<Props, State> {
20
+ state: State = { hasError: false };
21
+
22
+ static getDerivedStateFromError(error: unknown): State {
23
+ return {
24
+ hasError: true,
25
+ message: error instanceof Error ? error.message : String(error),
26
+ };
27
+ }
28
+
29
+ componentDidCatch(error: unknown, info: unknown) {
30
+ // eslint-disable-next-line no-console
31
+ console.warn("[openclaude-chat] part renderer failed:", error, info);
32
+ }
33
+
34
+ render() {
35
+ if (this.state.hasError) {
36
+ return (
37
+ <div
38
+ role="alert"
39
+ className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive"
40
+ >
41
+ <AlertTriangle className="size-3.5 shrink-0 mt-0.5" />
42
+ <span className="flex-1 min-w-0 break-words font-mono">
43
+ {this.props.label ? `[${this.props.label}] ` : ""}
44
+ {this.state.message ?? "Falha ao renderizar bloco"}
45
+ </span>
46
+ </div>
47
+ );
48
+ }
49
+ return this.props.children;
50
+ }
51
+ }
@@ -1,145 +1,145 @@
1
- import { memo, useState } from "react";
2
- import { Paperclip, ChevronDown } from "lucide-react";
3
- import { Markdown } from "../components/Markdown.js";
4
- import { LazyRender } from "../components/LazyRender.js";
5
- import { ReasoningBlock } from "./ReasoningBlock.js";
6
- import { ToolActivity } from "./ToolActivity.js";
7
- import { ToolResult } from "./ToolResult.js";
8
- import { resolveDisplayRenderer } from "../display/registry.js";
9
- import type { DisplayRendererMap } from "../display/registry.js";
10
- import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "../ui/collapsible.js";
11
- import { cn } from "../lib/utils.js";
12
- import type { MessagePart, TextPart, ReasoningPart, ToolInvocationPart } from "../types.js";
13
-
14
- const HEAVY_RENDERERS = new Set([
15
- "chart", "map", "table",
16
- "spreadsheet", "gallery", "image",
17
- ]);
18
-
19
- export interface PartRendererProps {
20
- part: MessagePart;
21
- isStreaming?: boolean;
22
- displayRenderers?: DisplayRendererMap;
23
- }
24
-
25
- // ─── Attachment sub-components ────────────────────────────────────────────────
26
-
27
- function AttachmentTextBlock({ filename, content }: { filename: string; content: string }) {
28
- const [open, setOpen] = useState(false);
29
- const lines = content.split("\n");
30
- const preview = lines.slice(0, 3).join("\n") + (lines.length > 3 && !open ? "\n…" : "");
31
-
32
- return (
33
- <Collapsible open={open} onOpenChange={setOpen} className="rounded-lg border text-xs overflow-hidden">
34
- <div className="flex items-center gap-2 px-3 py-2 bg-muted/50 border-b">
35
- <Paperclip className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
36
- <span className="flex-1 truncate font-medium">{filename}</span>
37
- <CollapsibleTrigger asChild>
38
- <button type="button" className="text-muted-foreground hover:text-foreground transition-colors" aria-label={open ? "Recolher" : "Expandir"}>
39
- <ChevronDown className={cn("h-3.5 w-3.5 transition-transform", open && "rotate-180")} />
40
- </button>
41
- </CollapsibleTrigger>
42
- </div>
43
- <pre className="px-3 py-2 text-muted-foreground whitespace-pre-wrap break-words">{preview}</pre>
44
- <CollapsibleContent>
45
- <pre className="px-3 py-2 max-h-40 overflow-auto whitespace-pre-wrap break-words border-t">{content}</pre>
46
- </CollapsibleContent>
47
- </Collapsible>
48
- );
49
- }
50
-
51
- // ─── Main renderer ────────────────────────────────────────────────────────────
52
-
53
- export const PartRenderer = memo(function PartRenderer({ part, isStreaming, displayRenderers }: PartRendererProps) {
54
- switch (part.type) {
55
- case "text": {
56
- const p = part as TextPart;
57
- if (p.text.startsWith("[📎")) {
58
- const firstNewline = p.text.indexOf("\n");
59
- const header = firstNewline >= 0 ? p.text.slice(0, firstNewline) : p.text;
60
- const body = firstNewline >= 0 ? p.text.slice(firstNewline + 1) : "";
61
- const match = header.match(/^\[📎\s+(.+?)\]?$/);
62
- const filename = match?.[1] ?? header;
63
- return <AttachmentTextBlock filename={filename} content={body} />;
64
- }
65
- return <Markdown>{p.text}</Markdown>;
66
- }
67
-
68
- case "reasoning": {
69
- const p = part as ReasoningPart;
70
- return <ReasoningBlock content={p.reasoning} isStreaming={isStreaming} />;
71
- }
72
-
73
- case "tool-invocation": {
74
- const { toolInvocation } = part as ToolInvocationPart;
75
- // O openclaude-sdk entrega os display tools como meta-tools MCP com
76
- // prefixo `mcp__display__display_*` (ex: mcp__display__display_highlight).
77
- // O tipo especifico do widget vem no campo `action` do input/args
78
- // (ex: {action: "metric", label: "...", value: "..."}).
79
- //
80
- // Alem disso aceitamos a forma "legada" direta `display_*` (compat).
81
- const name = toolInvocation.toolName;
82
- const isMcpDisplay = /^mcp__display__display_(highlight|collection|card|visual)$/.test(name);
83
- const isLegacyDisplay = name.startsWith("display_");
84
- const isDisplay = isMcpDisplay || isLegacyDisplay;
85
-
86
- if (isDisplay) {
87
- // IMPORTANTE: para display tools, o `args` (input) contem a definicao
88
- // COMPLETA do widget. O `result` (tool_result) e apenas uma confirmacao
89
- // minima (`{action: "metric"}`). Sempre preferimos args aqui.
90
- const payload = toolInvocation.args as Record<string, unknown> | undefined;
91
- // Para meta-tools MCP, a action vem no payload.
92
- // Para display_* legado, a action e o sufixo do toolName.
93
- const action = isMcpDisplay
94
- ? (payload?.action as string | undefined)
95
- : name.replace(/^display_/, "");
96
- const Renderer = action ? resolveDisplayRenderer(action, displayRenderers) : null;
97
- if (payload && Renderer && action) {
98
- // Alguns campos complexos (ex: trend, data) podem chegar serializados
99
- // como JSON string se o modelo nao souber passar objetos. Tentamos
100
- // parsear string → objeto antes de spreadar no renderer.
101
- const normalized: Record<string, unknown> = {};
102
- for (const [k, v] of Object.entries(payload)) {
103
- if (typeof v === "string" && (v.startsWith("{") || v.startsWith("["))) {
104
- try {
105
- normalized[k] = JSON.parse(v);
106
- } catch {
107
- normalized[k] = v;
108
- }
109
- } else {
110
- normalized[k] = v;
111
- }
112
- }
113
- // key by toolCallId so iframe-based renderers (DisplayReactRenderer)
114
- // unmount/remount cleanly when a new tool_use block arrives.
115
- const rendered = <Renderer key={toolInvocation.toolCallId} {...normalized} />;
116
- // NOTE: LazyRender via IntersectionObserver nao funciona bem dentro
117
- // do virtualized MessageList (itens ficam position:absolute e o
118
- // observer nao dispara consistente). Renderizamos direto.
119
- return rendered;
120
- }
121
- }
122
-
123
- if (toolInvocation.state === "result") {
124
- return (
125
- <ToolResult
126
- toolName={toolInvocation.toolName}
127
- result={toolInvocation.result}
128
- isError={toolInvocation.isError}
129
- />
130
- );
131
- }
132
-
133
- return (
134
- <ToolActivity
135
- toolName={toolInvocation.toolName}
136
- state={toolInvocation.state}
137
- args={toolInvocation.args}
138
- />
139
- );
140
- }
141
-
142
- default:
143
- return null;
144
- }
145
- });
1
+ import { memo, useState } from "react";
2
+ import { Paperclip, ChevronDown } from "lucide-react";
3
+ import { Markdown } from "../components/Markdown.js";
4
+ import { LazyRender } from "../components/LazyRender.js";
5
+ import { ReasoningBlock } from "./ReasoningBlock.js";
6
+ import { ToolActivity } from "./ToolActivity.js";
7
+ import { ToolResult } from "./ToolResult.js";
8
+ import { resolveDisplayRenderer } from "../display/registry.js";
9
+ import type { DisplayRendererMap } from "../display/registry.js";
10
+ import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "../ui/collapsible.js";
11
+ import { cn } from "../lib/utils.js";
12
+ import type { MessagePart, TextPart, ReasoningPart, ToolInvocationPart } from "../types.js";
13
+
14
+ const HEAVY_RENDERERS = new Set([
15
+ "chart", "map", "table",
16
+ "spreadsheet", "gallery", "image",
17
+ ]);
18
+
19
+ export interface PartRendererProps {
20
+ part: MessagePart;
21
+ isStreaming?: boolean;
22
+ displayRenderers?: DisplayRendererMap;
23
+ }
24
+
25
+ // ─── Attachment sub-components ────────────────────────────────────────────────
26
+
27
+ function AttachmentTextBlock({ filename, content }: { filename: string; content: string }) {
28
+ const [open, setOpen] = useState(false);
29
+ const lines = content.split("\n");
30
+ const preview = lines.slice(0, 3).join("\n") + (lines.length > 3 && !open ? "\n…" : "");
31
+
32
+ return (
33
+ <Collapsible open={open} onOpenChange={setOpen} className="rounded-lg border text-xs overflow-hidden">
34
+ <div className="flex items-center gap-2 px-3 py-2 bg-muted/50 border-b">
35
+ <Paperclip className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
36
+ <span className="flex-1 truncate font-medium">{filename}</span>
37
+ <CollapsibleTrigger asChild>
38
+ <button type="button" className="text-muted-foreground hover:text-foreground transition-colors" aria-label={open ? "Recolher" : "Expandir"}>
39
+ <ChevronDown className={cn("h-3.5 w-3.5 transition-transform", open && "rotate-180")} />
40
+ </button>
41
+ </CollapsibleTrigger>
42
+ </div>
43
+ <pre className="px-3 py-2 text-muted-foreground whitespace-pre-wrap break-words">{preview}</pre>
44
+ <CollapsibleContent>
45
+ <pre className="px-3 py-2 max-h-40 overflow-auto whitespace-pre-wrap break-words border-t">{content}</pre>
46
+ </CollapsibleContent>
47
+ </Collapsible>
48
+ );
49
+ }
50
+
51
+ // ─── Main renderer ────────────────────────────────────────────────────────────
52
+
53
+ export const PartRenderer = memo(function PartRenderer({ part, isStreaming, displayRenderers }: PartRendererProps) {
54
+ switch (part.type) {
55
+ case "text": {
56
+ const p = part as TextPart;
57
+ if (p.text.startsWith("[📎")) {
58
+ const firstNewline = p.text.indexOf("\n");
59
+ const header = firstNewline >= 0 ? p.text.slice(0, firstNewline) : p.text;
60
+ const body = firstNewline >= 0 ? p.text.slice(firstNewline + 1) : "";
61
+ const match = header.match(/^\[📎\s+(.+?)\]?$/);
62
+ const filename = match?.[1] ?? header;
63
+ return <AttachmentTextBlock filename={filename} content={body} />;
64
+ }
65
+ return <Markdown>{p.text}</Markdown>;
66
+ }
67
+
68
+ case "reasoning": {
69
+ const p = part as ReasoningPart;
70
+ return <ReasoningBlock content={p.reasoning} isStreaming={isStreaming} />;
71
+ }
72
+
73
+ case "tool-invocation": {
74
+ const { toolInvocation } = part as ToolInvocationPart;
75
+ // O openclaude-sdk entrega os display tools como meta-tools MCP com
76
+ // prefixo `mcp__display__display_*` (ex: mcp__display__display_highlight).
77
+ // O tipo especifico do widget vem no campo `action` do input/args
78
+ // (ex: {action: "metric", label: "...", value: "..."}).
79
+ //
80
+ // Alem disso aceitamos a forma "legada" direta `display_*` (compat).
81
+ const name = toolInvocation.toolName;
82
+ const isMcpDisplay = /^mcp__display__display_(highlight|collection|card|visual)$/.test(name);
83
+ const isLegacyDisplay = name.startsWith("display_");
84
+ const isDisplay = isMcpDisplay || isLegacyDisplay;
85
+
86
+ if (isDisplay) {
87
+ // IMPORTANTE: para display tools, o `args` (input) contem a definicao
88
+ // COMPLETA do widget. O `result` (tool_result) e apenas uma confirmacao
89
+ // minima (`{action: "metric"}`). Sempre preferimos args aqui.
90
+ const payload = toolInvocation.args as Record<string, unknown> | undefined;
91
+ // Para meta-tools MCP, a action vem no payload.
92
+ // Para display_* legado, a action e o sufixo do toolName.
93
+ const action = isMcpDisplay
94
+ ? (payload?.action as string | undefined)
95
+ : name.replace(/^display_/, "");
96
+ const Renderer = action ? resolveDisplayRenderer(action, displayRenderers) : null;
97
+ if (payload && Renderer && action) {
98
+ // Alguns campos complexos (ex: trend, data) podem chegar serializados
99
+ // como JSON string se o modelo nao souber passar objetos. Tentamos
100
+ // parsear string → objeto antes de spreadar no renderer.
101
+ const normalized: Record<string, unknown> = {};
102
+ for (const [k, v] of Object.entries(payload)) {
103
+ if (typeof v === "string" && (v.startsWith("{") || v.startsWith("["))) {
104
+ try {
105
+ normalized[k] = JSON.parse(v);
106
+ } catch {
107
+ normalized[k] = v;
108
+ }
109
+ } else {
110
+ normalized[k] = v;
111
+ }
112
+ }
113
+ // key by toolCallId so iframe-based renderers (DisplayReactRenderer)
114
+ // unmount/remount cleanly when a new tool_use block arrives.
115
+ const rendered = <Renderer key={toolInvocation.toolCallId} {...normalized} />;
116
+ // NOTE: LazyRender via IntersectionObserver nao funciona bem dentro
117
+ // do virtualized MessageList (itens ficam position:absolute e o
118
+ // observer nao dispara consistente). Renderizamos direto.
119
+ return rendered;
120
+ }
121
+ }
122
+
123
+ if (toolInvocation.state === "result") {
124
+ return (
125
+ <ToolResult
126
+ toolName={toolInvocation.toolName}
127
+ result={toolInvocation.result}
128
+ isError={toolInvocation.isError}
129
+ />
130
+ );
131
+ }
132
+
133
+ return (
134
+ <ToolActivity
135
+ toolName={toolInvocation.toolName}
136
+ state={toolInvocation.state}
137
+ args={toolInvocation.args}
138
+ />
139
+ );
140
+ }
141
+
142
+ default:
143
+ return null;
144
+ }
145
+ });
@@ -1,41 +1,41 @@
1
- import { useEffect, useState } from "react";
2
- import { Brain, ChevronDown, ChevronRight } from "lucide-react";
3
- import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
4
-
5
- export interface ReasoningBlockProps {
6
- content: string;
7
- isStreaming?: boolean;
8
- className?: string;
9
- }
10
-
11
- export function ReasoningBlock({ content, isStreaming = false, className }: ReasoningBlockProps) {
12
- const [expanded, setExpanded] = useState(isStreaming);
13
-
14
- useEffect(() => {
15
- if (isStreaming) {
16
- setExpanded(true);
17
- } else {
18
- setExpanded(false);
19
- }
20
- }, [isStreaming]);
21
-
22
- return (
23
- <Collapsible open={expanded} onOpenChange={setExpanded} className={className}>
24
- <div className="border border-border bg-muted/30 rounded-md text-sm overflow-hidden">
25
- <CollapsibleTrigger className="flex items-center gap-2 px-3 py-2 w-full text-left font-medium text-muted-foreground hover:bg-muted/60 cursor-pointer">
26
- <Brain className="h-3.5 w-3.5" aria-hidden="true" />
27
- <span className="font-mono text-xs">reasoning</span>
28
- {expanded
29
- ? <ChevronDown className="h-3.5 w-3.5 ml-auto" aria-hidden="true" />
30
- : <ChevronRight className="h-3.5 w-3.5 ml-auto" aria-hidden="true" />
31
- }
32
- </CollapsibleTrigger>
33
- <CollapsibleContent>
34
- <div className="max-h-96 overflow-y-auto px-3 py-3 text-muted-foreground whitespace-pre-wrap break-words border-t border-border text-xs">
35
- {content}
36
- </div>
37
- </CollapsibleContent>
38
- </div>
39
- </Collapsible>
40
- );
41
- }
1
+ import { useEffect, useState } from "react";
2
+ import { Brain, ChevronDown, ChevronRight } from "lucide-react";
3
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
4
+
5
+ export interface ReasoningBlockProps {
6
+ content: string;
7
+ isStreaming?: boolean;
8
+ className?: string;
9
+ }
10
+
11
+ export function ReasoningBlock({ content, isStreaming = false, className }: ReasoningBlockProps) {
12
+ const [expanded, setExpanded] = useState(isStreaming);
13
+
14
+ useEffect(() => {
15
+ if (isStreaming) {
16
+ setExpanded(true);
17
+ } else {
18
+ setExpanded(false);
19
+ }
20
+ }, [isStreaming]);
21
+
22
+ return (
23
+ <Collapsible open={expanded} onOpenChange={setExpanded} className={className}>
24
+ <div className="border border-border bg-muted/30 rounded-md text-sm overflow-hidden">
25
+ <CollapsibleTrigger className="flex items-center gap-2 px-3 py-2 w-full text-left font-medium text-muted-foreground hover:bg-muted/60 cursor-pointer">
26
+ <Brain className="h-3.5 w-3.5" aria-hidden="true" />
27
+ <span className="font-mono text-xs">reasoning</span>
28
+ {expanded
29
+ ? <ChevronDown className="h-3.5 w-3.5 ml-auto" aria-hidden="true" />
30
+ : <ChevronRight className="h-3.5 w-3.5 ml-auto" aria-hidden="true" />
31
+ }
32
+ </CollapsibleTrigger>
33
+ <CollapsibleContent>
34
+ <div className="max-h-96 overflow-y-auto px-3 py-3 text-muted-foreground whitespace-pre-wrap break-words border-t border-border text-xs">
35
+ {content}
36
+ </div>
37
+ </CollapsibleContent>
38
+ </div>
39
+ </Collapsible>
40
+ );
41
+ }