@codrstudio/openclaude-chat 0.1.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 (171) hide show
  1. package/dist/components/Chat.d.ts +23 -0
  2. package/dist/components/Chat.js +12 -0
  3. package/dist/components/ErrorNote.d.ts +6 -0
  4. package/dist/components/ErrorNote.js +6 -0
  5. package/dist/components/LazyRender.d.ts +8 -0
  6. package/dist/components/LazyRender.js +22 -0
  7. package/dist/components/Markdown.d.ts +5 -0
  8. package/dist/components/Markdown.js +65 -0
  9. package/dist/components/MessageBubble.d.ts +9 -0
  10. package/dist/components/MessageBubble.js +45 -0
  11. package/dist/components/MessageInput.d.ts +19 -0
  12. package/dist/components/MessageInput.js +214 -0
  13. package/dist/components/MessageList.d.ts +13 -0
  14. package/dist/components/MessageList.js +72 -0
  15. package/dist/components/StreamingIndicator.d.ts +1 -0
  16. package/dist/components/StreamingIndicator.js +9 -0
  17. package/dist/display/AlertRenderer.d.ts +2 -0
  18. package/dist/display/AlertRenderer.js +13 -0
  19. package/dist/display/CarouselRenderer.d.ts +2 -0
  20. package/dist/display/CarouselRenderer.js +41 -0
  21. package/dist/display/ChartRenderer.d.ts +2 -0
  22. package/dist/display/ChartRenderer.js +76 -0
  23. package/dist/display/ChoiceButtonsRenderer.d.ts +6 -0
  24. package/dist/display/ChoiceButtonsRenderer.js +23 -0
  25. package/dist/display/CodeBlockRenderer.d.ts +2 -0
  26. package/dist/display/CodeBlockRenderer.js +17 -0
  27. package/dist/display/ComparisonTableRenderer.d.ts +2 -0
  28. package/dist/display/ComparisonTableRenderer.js +26 -0
  29. package/dist/display/DataTableRenderer.d.ts +2 -0
  30. package/dist/display/DataTableRenderer.js +74 -0
  31. package/dist/display/DisplayReactRenderer.d.ts +26 -0
  32. package/dist/display/DisplayReactRenderer.js +192 -0
  33. package/dist/display/FileCardRenderer.d.ts +2 -0
  34. package/dist/display/FileCardRenderer.js +31 -0
  35. package/dist/display/GalleryRenderer.d.ts +2 -0
  36. package/dist/display/GalleryRenderer.js +11 -0
  37. package/dist/display/ImageViewerRenderer.d.ts +2 -0
  38. package/dist/display/ImageViewerRenderer.js +15 -0
  39. package/dist/display/LinkPreviewRenderer.d.ts +2 -0
  40. package/dist/display/LinkPreviewRenderer.js +20 -0
  41. package/dist/display/MapViewRenderer.d.ts +2 -0
  42. package/dist/display/MapViewRenderer.js +20 -0
  43. package/dist/display/MetricCardRenderer.d.ts +2 -0
  44. package/dist/display/MetricCardRenderer.js +12 -0
  45. package/dist/display/PriceHighlightRenderer.d.ts +2 -0
  46. package/dist/display/PriceHighlightRenderer.js +30 -0
  47. package/dist/display/ProductCardRenderer.d.ts +2 -0
  48. package/dist/display/ProductCardRenderer.js +23 -0
  49. package/dist/display/ProgressStepsRenderer.d.ts +2 -0
  50. package/dist/display/ProgressStepsRenderer.js +14 -0
  51. package/dist/display/SourcesListRenderer.d.ts +2 -0
  52. package/dist/display/SourcesListRenderer.js +5 -0
  53. package/dist/display/SpreadsheetRenderer.d.ts +2 -0
  54. package/dist/display/SpreadsheetRenderer.js +32 -0
  55. package/dist/display/StepTimelineRenderer.d.ts +2 -0
  56. package/dist/display/StepTimelineRenderer.js +21 -0
  57. package/dist/display/index.d.ts +21 -0
  58. package/dist/display/index.js +20 -0
  59. package/dist/display/react-sandbox/bootstrap.d.ts +1 -0
  60. package/dist/display/react-sandbox/bootstrap.js +154 -0
  61. package/dist/display/registry.d.ts +5 -0
  62. package/dist/display/registry.js +52 -0
  63. package/dist/display/sdk-types.d.ts +187 -0
  64. package/dist/display/sdk-types.js +4 -0
  65. package/dist/hooks/ChatProvider.d.ts +9 -0
  66. package/dist/hooks/ChatProvider.js +14 -0
  67. package/dist/hooks/useIsMobile.d.ts +1 -0
  68. package/dist/hooks/useIsMobile.js +12 -0
  69. package/dist/hooks/useOpenClaudeChat.d.ts +36 -0
  70. package/dist/hooks/useOpenClaudeChat.js +361 -0
  71. package/dist/index.d.ts +47 -0
  72. package/dist/index.js +42 -0
  73. package/dist/lib/utils.d.ts +2 -0
  74. package/dist/lib/utils.js +5 -0
  75. package/dist/parts/PartErrorBoundary.d.ts +21 -0
  76. package/dist/parts/PartErrorBoundary.js +27 -0
  77. package/dist/parts/PartRenderer.d.ts +8 -0
  78. package/dist/parts/PartRenderer.js +99 -0
  79. package/dist/parts/ReasoningBlock.d.ts +6 -0
  80. package/dist/parts/ReasoningBlock.js +18 -0
  81. package/dist/parts/ToolActivity.d.ts +11 -0
  82. package/dist/parts/ToolActivity.js +52 -0
  83. package/dist/parts/ToolResult.d.ts +7 -0
  84. package/dist/parts/ToolResult.js +38 -0
  85. package/dist/styles.css +2 -0
  86. package/dist/types.d.ts +40 -0
  87. package/dist/types.js +4 -0
  88. package/dist/ui/alert.d.ts +12 -0
  89. package/dist/ui/alert.js +28 -0
  90. package/dist/ui/badge.d.ts +9 -0
  91. package/dist/ui/badge.js +20 -0
  92. package/dist/ui/button.d.ts +11 -0
  93. package/dist/ui/button.js +31 -0
  94. package/dist/ui/card.d.ts +8 -0
  95. package/dist/ui/card.js +21 -0
  96. package/dist/ui/collapsible.d.ts +1 -0
  97. package/dist/ui/collapsible.js +2 -0
  98. package/dist/ui/dialog.d.ts +19 -0
  99. package/dist/ui/dialog.js +23 -0
  100. package/dist/ui/dropdown-menu.d.ts +11 -0
  101. package/dist/ui/dropdown-menu.js +15 -0
  102. package/dist/ui/input.d.ts +3 -0
  103. package/dist/ui/input.js +6 -0
  104. package/dist/ui/progress.d.ts +7 -0
  105. package/dist/ui/progress.js +9 -0
  106. package/dist/ui/scroll-area.d.ts +5 -0
  107. package/dist/ui/scroll-area.js +12 -0
  108. package/dist/ui/separator.d.ts +4 -0
  109. package/dist/ui/separator.js +8 -0
  110. package/dist/ui/skeleton.d.ts +3 -0
  111. package/dist/ui/skeleton.js +6 -0
  112. package/dist/ui/table.d.ts +10 -0
  113. package/dist/ui/table.js +27 -0
  114. package/package.json +61 -0
  115. package/src/components/Chat.tsx +107 -0
  116. package/src/components/ErrorNote.tsx +35 -0
  117. package/src/components/LazyRender.tsx +42 -0
  118. package/src/components/Markdown.tsx +114 -0
  119. package/src/components/MessageBubble.tsx +107 -0
  120. package/src/components/MessageInput.tsx +421 -0
  121. package/src/components/MessageList.tsx +153 -0
  122. package/src/components/StreamingIndicator.tsx +19 -0
  123. package/src/display/AlertRenderer.tsx +23 -0
  124. package/src/display/CarouselRenderer.tsx +141 -0
  125. package/src/display/ChartRenderer.tsx +195 -0
  126. package/src/display/ChoiceButtonsRenderer.tsx +114 -0
  127. package/src/display/CodeBlockRenderer.tsx +49 -0
  128. package/src/display/ComparisonTableRenderer.tsx +132 -0
  129. package/src/display/DataTableRenderer.tsx +144 -0
  130. package/src/display/DisplayReactRenderer.tsx +269 -0
  131. package/src/display/FileCardRenderer.tsx +55 -0
  132. package/src/display/GalleryRenderer.tsx +65 -0
  133. package/src/display/ImageViewerRenderer.tsx +114 -0
  134. package/src/display/LinkPreviewRenderer.tsx +74 -0
  135. package/src/display/MapViewRenderer.tsx +75 -0
  136. package/src/display/MetricCardRenderer.tsx +29 -0
  137. package/src/display/PriceHighlightRenderer.tsx +62 -0
  138. package/src/display/ProductCardRenderer.tsx +112 -0
  139. package/src/display/ProgressStepsRenderer.tsx +59 -0
  140. package/src/display/SourcesListRenderer.tsx +47 -0
  141. package/src/display/SpreadsheetRenderer.tsx +86 -0
  142. package/src/display/StepTimelineRenderer.tsx +75 -0
  143. package/src/display/index.ts +21 -0
  144. package/src/display/react-sandbox/bootstrap.ts +155 -0
  145. package/src/display/registry.ts +84 -0
  146. package/src/display/sdk-types.ts +217 -0
  147. package/src/hooks/ChatProvider.tsx +21 -0
  148. package/src/hooks/useIsMobile.ts +15 -0
  149. package/src/hooks/useOpenClaudeChat.ts +476 -0
  150. package/src/index.ts +76 -0
  151. package/src/lib/utils.ts +6 -0
  152. package/src/parts/PartErrorBoundary.tsx +51 -0
  153. package/src/parts/PartRenderer.tsx +145 -0
  154. package/src/parts/ReasoningBlock.tsx +41 -0
  155. package/src/parts/ToolActivity.tsx +78 -0
  156. package/src/parts/ToolResult.tsx +79 -0
  157. package/src/styles.css +2 -0
  158. package/src/types.ts +41 -0
  159. package/src/ui/alert.tsx +77 -0
  160. package/src/ui/badge.tsx +36 -0
  161. package/src/ui/button.tsx +54 -0
  162. package/src/ui/card.tsx +68 -0
  163. package/src/ui/collapsible.tsx +7 -0
  164. package/src/ui/dialog.tsx +122 -0
  165. package/src/ui/dropdown-menu.tsx +76 -0
  166. package/src/ui/input.tsx +24 -0
  167. package/src/ui/progress.tsx +36 -0
  168. package/src/ui/scroll-area.tsx +48 -0
  169. package/src/ui/separator.tsx +31 -0
  170. package/src/ui/skeleton.tsx +9 -0
  171. package/src/ui/table.tsx +114 -0
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ import { useOpenClaudeChat, type UseOpenClaudeChatOptions } from "./useOpenClaudeChat.js";
3
+ type ChatContextValue = ReturnType<typeof useOpenClaudeChat>;
4
+ export interface ChatProviderProps extends UseOpenClaudeChatOptions {
5
+ children: React.ReactNode;
6
+ }
7
+ export declare function ChatProvider({ children, ...options }: ChatProviderProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function useChatContext(): ChatContextValue;
9
+ export {};
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ import { useOpenClaudeChat } from "./useOpenClaudeChat.js";
4
+ const ChatContext = createContext(null);
5
+ export function ChatProvider({ children, ...options }) {
6
+ const chat = useOpenClaudeChat(options);
7
+ return _jsx(ChatContext.Provider, { value: chat, children: children });
8
+ }
9
+ export function useChatContext() {
10
+ const ctx = useContext(ChatContext);
11
+ if (!ctx)
12
+ throw new Error("useChatContext must be used within ChatProvider");
13
+ return ctx;
14
+ }
@@ -0,0 +1 @@
1
+ export declare function useIsMobile(breakpoint?: number): boolean;
@@ -0,0 +1,12 @@
1
+ import { useState, useEffect } from "react";
2
+ export function useIsMobile(breakpoint = 768) {
3
+ const [isMobile, setIsMobile] = useState(false);
4
+ useEffect(() => {
5
+ const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
6
+ const onChange = () => setIsMobile(mql.matches);
7
+ onChange();
8
+ mql.addEventListener("change", onChange);
9
+ return () => mql.removeEventListener("change", onChange);
10
+ }, [breakpoint]);
11
+ return isMobile;
12
+ }
@@ -0,0 +1,36 @@
1
+ import type { Message } from "../types.js";
2
+ export interface UseOpenClaudeChatOptions {
3
+ /** Base URL do servico que encapsula o openclaude-sdk. Ex: http://localhost:9500/api/v1/ai */
4
+ endpoint: string;
5
+ /** Optional bearer token. */
6
+ token?: string;
7
+ /**
8
+ * ID de sessao live. Caso omitido, o hook cria uma sessao nova via POST /sessions
9
+ * na primeira mensagem e expoe o id via `sessionId`.
10
+ */
11
+ sessionId?: string;
12
+ /** Mensagens iniciais (historico) para hidratar a UI. */
13
+ initialMessages?: Message[];
14
+ /** Options extras passadas ao servidor na criacao da sessao. */
15
+ sessionOptions?: Record<string, unknown>;
16
+ /** Options por turno. Passado como `turnOptions` no body do /prompt. */
17
+ turnOptions?: Record<string, unknown>;
18
+ /** Customiza fetch (ex: para injetar credenciais). */
19
+ fetcher?: typeof fetch;
20
+ }
21
+ export interface UseOpenClaudeChatReturn {
22
+ sessionId: string | null;
23
+ messages: Message[];
24
+ input: string;
25
+ setInput: (value: string) => void;
26
+ isLoading: boolean;
27
+ error: Error | null;
28
+ handleSubmit: (e?: {
29
+ preventDefault?: () => void;
30
+ }) => void;
31
+ sendMessage: (text: string) => Promise<void>;
32
+ stop: () => void;
33
+ reload: () => Promise<void>;
34
+ clear: () => void;
35
+ }
36
+ export declare function useOpenClaudeChat(options: UseOpenClaudeChatOptions): UseOpenClaudeChatReturn;
@@ -0,0 +1,361 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ function extractTextFromParts(parts) {
4
+ return parts
5
+ .filter((p) => p.type === "text")
6
+ .map((p) => p.text)
7
+ .join("\n");
8
+ }
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ export function useOpenClaudeChat(options) {
11
+ const { endpoint, token, sessionId: providedSessionId, initialMessages, sessionOptions, turnOptions, fetcher, } = options;
12
+ const [sessionId, setSessionId] = useState(providedSessionId ?? null);
13
+ const [messages, setMessages] = useState(initialMessages ?? []);
14
+ const [input, setInput] = useState("");
15
+ const [isLoading, setIsLoading] = useState(false);
16
+ const [error, setError] = useState(null);
17
+ const abortRef = useRef(null);
18
+ const lastSentRef = useRef(null);
19
+ const doFetch = fetcher ?? fetch;
20
+ // ──────────────────────────────────────────────────────────────
21
+ // Garante sessao live
22
+ const ensureSession = useCallback(async () => {
23
+ if (sessionId)
24
+ return sessionId;
25
+ const headers = { "Content-Type": "application/json" };
26
+ if (token)
27
+ headers.Authorization = `Bearer ${token}`;
28
+ const res = await doFetch(`${endpoint}/sessions`, {
29
+ method: "POST",
30
+ headers,
31
+ body: JSON.stringify({ options: sessionOptions ?? {} }),
32
+ });
33
+ if (!res.ok) {
34
+ throw new Error(`Failed to create session: HTTP ${res.status}`);
35
+ }
36
+ const data = (await res.json());
37
+ setSessionId(data.sessionId);
38
+ return data.sessionId;
39
+ }, [sessionId, endpoint, token, sessionOptions, doFetch]);
40
+ // ──────────────────────────────────────────────────────────────
41
+ // Stream parser — consome SSE do endpoint /sessions/:id/prompt
42
+ // Contrato: SEMPRE promove um assistantId no setMessages e SEMPRE retorna
43
+ // o numero de partes acumuladas. O chamador decide se isso configura erro
44
+ // (ex: zero partes = resposta vazia, mesmo com HTTP 200).
45
+ const streamPrompt = useCallback(async (sid, text) => {
46
+ const headers = {
47
+ "Content-Type": "application/json",
48
+ Accept: "text/event-stream",
49
+ };
50
+ if (token)
51
+ headers.Authorization = `Bearer ${token}`;
52
+ const ctrl = new AbortController();
53
+ abortRef.current = ctrl;
54
+ // Watchdog de stall — se nao chegar NENHUM byte SSE (incluindo ping) em
55
+ // STALL_MS, considera sessao morta e aborta. Subimos pra 90s pra tolerar
56
+ // chat agentico (LLM pode pensar muito entre tool calls). O servidor
57
+ // emite `event: ping` periodicamente como heartbeat — qualquer chunk SSE
58
+ // (ping ou message) renova esse timer via armStall().
59
+ const STALL_MS = 90_000;
60
+ let stallTimer = null;
61
+ const armStall = () => {
62
+ if (stallTimer)
63
+ clearTimeout(stallTimer);
64
+ stallTimer = setTimeout(() => {
65
+ try {
66
+ ctrl.abort(new Error("stall-timeout"));
67
+ }
68
+ catch { /* noop */ }
69
+ }, STALL_MS);
70
+ };
71
+ const disarmStall = () => {
72
+ if (stallTimer)
73
+ clearTimeout(stallTimer);
74
+ stallTimer = null;
75
+ };
76
+ // Cria placeholder assistant ANTES do fetch — garante feedback visual imediato
77
+ // mesmo em caso de rede lenta ou erro no fetch.
78
+ const assistantId = `asst-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
79
+ setMessages((prev) => [
80
+ ...prev,
81
+ { id: assistantId, role: "assistant", content: "", parts: [] },
82
+ ]);
83
+ armStall();
84
+ let res;
85
+ try {
86
+ res = await doFetch(`${endpoint}/sessions/${encodeURIComponent(sid)}/prompt`, {
87
+ method: "POST",
88
+ headers,
89
+ body: JSON.stringify({ prompt: text, turnOptions }),
90
+ signal: ctrl.signal,
91
+ });
92
+ }
93
+ catch (err) {
94
+ disarmStall();
95
+ throw err;
96
+ }
97
+ if (!res.ok || !res.body) {
98
+ disarmStall();
99
+ const errText = await res.text().catch(() => `HTTP ${res.status}`);
100
+ throw new Error(errText || `HTTP ${res.status}`);
101
+ }
102
+ const reader = res.body.getReader();
103
+ const decoder = new TextDecoder();
104
+ let buffer = "";
105
+ // Buffer acumulado de partes — fonte da verdade para esta sessao de stream.
106
+ // Mantemos fora do functional updater do setMessages (o React 19 StrictMode
107
+ // chama o updater mais de uma vez e mutar estado compartilhado dentro dele
108
+ // leva a blocos perdidos).
109
+ const accumulatedParts = [];
110
+ const toolPartByCallId = new Map();
111
+ const commit = () => {
112
+ const snapshot = accumulatedParts.map((p) => ({ ...p }));
113
+ setMessages((prev) => {
114
+ const idx = prev.findIndex((m) => m.id === assistantId);
115
+ if (idx === -1)
116
+ return prev;
117
+ const next = prev.slice();
118
+ next[idx] = {
119
+ ...prev[idx],
120
+ parts: snapshot,
121
+ content: extractTextFromParts(snapshot),
122
+ };
123
+ return next;
124
+ });
125
+ };
126
+ const handleEvent = (eventName, dataStr) => {
127
+ if (eventName === "error") {
128
+ try {
129
+ const payload = JSON.parse(dataStr);
130
+ throw new Error(payload.message ?? "Stream error");
131
+ }
132
+ catch (err) {
133
+ throw err instanceof Error ? err : new Error(String(err));
134
+ }
135
+ }
136
+ if (eventName === "done")
137
+ return;
138
+ // Heartbeat do servidor — `event: ping` apenas serve para manter o
139
+ // stall watchdog renovado durante pausas longas do agente. Nao tem
140
+ // payload util; o efeito ja foi aplicado no loop ao chamar armStall()
141
+ // a cada chunk recebido. Apenas retornamos sem processar.
142
+ if (eventName === "ping" || eventName === "keepalive" || eventName === "heartbeat")
143
+ return;
144
+ if (eventName !== "message")
145
+ return;
146
+ let msg;
147
+ try {
148
+ msg = JSON.parse(dataStr);
149
+ }
150
+ catch {
151
+ return;
152
+ }
153
+ if (msg.type === "assistant") {
154
+ const assistant = msg;
155
+ const blocks = assistant.message?.content ?? [];
156
+ for (const block of blocks) {
157
+ if (block.type === "text") {
158
+ accumulatedParts.push({ type: "text", text: block.text });
159
+ }
160
+ else if (block.type === "tool_use") {
161
+ if (toolPartByCallId.has(block.id))
162
+ continue;
163
+ const part = {
164
+ type: "tool-invocation",
165
+ toolInvocation: {
166
+ toolName: block.name,
167
+ toolCallId: block.id,
168
+ state: "call",
169
+ args: block.input,
170
+ },
171
+ };
172
+ toolPartByCallId.set(block.id, part);
173
+ accumulatedParts.push(part);
174
+ }
175
+ }
176
+ commit();
177
+ return;
178
+ }
179
+ if (msg.type === "user") {
180
+ // Normalmente traz tool_result com ref a tool_use anterior.
181
+ const userMsg = msg;
182
+ const content = userMsg.message?.content;
183
+ if (Array.isArray(content)) {
184
+ let changed = false;
185
+ for (const block of content) {
186
+ if (block.type === "tool_result") {
187
+ const part = toolPartByCallId.get(block.tool_use_id);
188
+ if (part) {
189
+ part.toolInvocation = {
190
+ ...part.toolInvocation,
191
+ state: "result",
192
+ result: block.content,
193
+ isError: block.is_error,
194
+ };
195
+ changed = true;
196
+ }
197
+ }
198
+ }
199
+ if (changed)
200
+ commit();
201
+ }
202
+ return;
203
+ }
204
+ if (msg.type === "result") {
205
+ // fim do turno — nada a fazer aqui; o loop quebra no "done"
206
+ return;
207
+ }
208
+ };
209
+ try {
210
+ while (true) {
211
+ const { done, value } = await reader.read();
212
+ if (done)
213
+ break;
214
+ armStall(); // reset watchdog a cada chunk que chega
215
+ buffer += decoder.decode(value, { stream: true });
216
+ // SSE frames: "event: X\ndata: Y\n\n"
217
+ let boundary;
218
+ while ((boundary = buffer.indexOf("\n\n")) !== -1) {
219
+ const frame = buffer.slice(0, boundary);
220
+ buffer = buffer.slice(boundary + 2);
221
+ let eventName = "message";
222
+ const dataLines = [];
223
+ for (const line of frame.split("\n")) {
224
+ if (line.startsWith("event:"))
225
+ eventName = line.slice(6).trim();
226
+ else if (line.startsWith("data:"))
227
+ dataLines.push(line.slice(5).trimStart());
228
+ }
229
+ if (dataLines.length === 0)
230
+ continue;
231
+ handleEvent(eventName, dataLines.join("\n"));
232
+ }
233
+ }
234
+ }
235
+ finally {
236
+ disarmStall();
237
+ abortRef.current = null;
238
+ }
239
+ return { assistantId, partCount: accumulatedParts.length };
240
+ }, [endpoint, token, turnOptions, doFetch]);
241
+ // ──────────────────────────────────────────────────────────────
242
+ // Helper: garante que NUNCA fica uma bolha de assistant vazia pendurada.
243
+ // Se o turno terminou em erro ou em stream vazio, removemos o placeholder
244
+ // (para o ErrorNote abaixo da mensagem do usuario aparecer) ou injetamos
245
+ // um bloco de texto com a mensagem de erro para o usuario.
246
+ const dropEmptyAssistant = useCallback((assistantId) => {
247
+ if (!assistantId)
248
+ return;
249
+ setMessages((prev) => {
250
+ const idx = prev.findIndex((m) => m.id === assistantId);
251
+ if (idx === -1)
252
+ return prev;
253
+ const msg = prev[idx];
254
+ if (msg.role === "assistant" && msg.parts.length === 0) {
255
+ const next = prev.slice();
256
+ next.splice(idx, 1);
257
+ return next;
258
+ }
259
+ return prev;
260
+ });
261
+ }, []);
262
+ const sendMessage = useCallback(async (text) => {
263
+ const trimmed = text.trim();
264
+ if (!trimmed || isLoading)
265
+ return;
266
+ setError(null);
267
+ lastSentRef.current = trimmed;
268
+ const userId = `user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
269
+ setMessages((prev) => [
270
+ ...prev,
271
+ {
272
+ id: userId,
273
+ role: "user",
274
+ content: trimmed,
275
+ parts: [{ type: "text", text: trimmed }],
276
+ },
277
+ ]);
278
+ setIsLoading(true);
279
+ // NOTE: removido o `HARD_LIMIT_MS` do turno inteiro. Em chat agentico o
280
+ // agente pode pensar/rodar tools por minutos sem ser uma falha. A unica
281
+ // razao para abortar e:
282
+ // - usuario clicou stop() (flag __userStop)
283
+ // - stall watchdog (sem nenhum byte SSE em STALL_MS, incluindo pings
284
+ // do servidor — heartbeat server-push e como sabemos que a sessao
285
+ // ainda esta viva mesmo durante pausas longas do LLM)
286
+ // - servidor emitiu `event: error` ou fechou o stream com 0 partes
287
+ let assistantIdForCleanup = null;
288
+ try {
289
+ const sid = await ensureSession();
290
+ const { assistantId, partCount } = await streamPrompt(sid, trimmed);
291
+ assistantIdForCleanup = assistantId;
292
+ if (partCount === 0) {
293
+ // Servidor fechou o stream sem emitir nenhum bloco — erro "macio".
294
+ throw new Error("O servidor nao retornou nenhum conteudo. Verifique se o provider de LLM esta configurado.");
295
+ }
296
+ }
297
+ catch (err) {
298
+ const e = err instanceof Error ? err : new Error(String(err));
299
+ // Aborts originados do `stop()` do usuario sao intencionais — nao viram erro.
300
+ const isUserStop = e.name === "AbortError" && e.__userStop;
301
+ if (!isUserStop) {
302
+ // Se foi stall/timeout de abort, troca a mensagem por algo legivel.
303
+ const msg = e.name === "AbortError"
304
+ ? "Tempo esgotado aguardando resposta do servidor."
305
+ : e.message || "Falha desconhecida ao processar mensagem.";
306
+ setError(new Error(msg));
307
+ }
308
+ dropEmptyAssistant(assistantIdForCleanup);
309
+ }
310
+ finally {
311
+ setIsLoading(false);
312
+ }
313
+ }, [isLoading, ensureSession, streamPrompt, dropEmptyAssistant]);
314
+ const handleSubmit = useCallback((e) => {
315
+ e?.preventDefault?.();
316
+ const text = input;
317
+ setInput("");
318
+ void sendMessage(text);
319
+ }, [input, sendMessage]);
320
+ const stop = useCallback(() => {
321
+ const err = new Error("User stopped stream");
322
+ err.name = "AbortError";
323
+ err.__userStop = true;
324
+ abortRef.current?.abort(err);
325
+ abortRef.current = null;
326
+ setIsLoading(false);
327
+ }, []);
328
+ const reload = useCallback(async () => {
329
+ if (!lastSentRef.current)
330
+ return;
331
+ // remove a ultima mensagem do assistant (se existir) antes de re-enviar
332
+ setMessages((prev) => {
333
+ if (prev.length === 0)
334
+ return prev;
335
+ const last = prev[prev.length - 1];
336
+ return last.role === "assistant" ? prev.slice(0, -1) : prev;
337
+ });
338
+ await sendMessage(lastSentRef.current);
339
+ }, [sendMessage]);
340
+ const clear = useCallback(() => {
341
+ setMessages([]);
342
+ setError(null);
343
+ lastSentRef.current = null;
344
+ }, []);
345
+ useEffect(() => {
346
+ return () => abortRef.current?.abort();
347
+ }, []);
348
+ return {
349
+ sessionId,
350
+ messages,
351
+ input,
352
+ setInput,
353
+ isLoading,
354
+ error,
355
+ handleSubmit,
356
+ sendMessage,
357
+ stop,
358
+ reload,
359
+ clear,
360
+ };
361
+ }
@@ -0,0 +1,47 @@
1
+ export type { Message, MessagePart, MessageRole, TextPart, ReasoningPart, ToolInvocationPart, ToolInvocationState, } from "./types.js";
2
+ export { Chat } from "./components/Chat.js";
3
+ export type { ChatProps } from "./components/Chat.js";
4
+ export { useOpenClaudeChat } from "./hooks/useOpenClaudeChat.js";
5
+ export type { UseOpenClaudeChatOptions, UseOpenClaudeChatReturn, } from "./hooks/useOpenClaudeChat.js";
6
+ export { ChatProvider, useChatContext } from "./hooks/ChatProvider.js";
7
+ export type { ChatProviderProps } from "./hooks/ChatProvider.js";
8
+ export { Markdown } from "./components/Markdown.js";
9
+ export { StreamingIndicator } from "./components/StreamingIndicator.js";
10
+ export { ErrorNote } from "./components/ErrorNote.js";
11
+ export type { ErrorNoteProps } from "./components/ErrorNote.js";
12
+ export { MessageBubble } from "./components/MessageBubble.js";
13
+ export type { MessageBubbleProps } from "./components/MessageBubble.js";
14
+ export { MessageList } from "./components/MessageList.js";
15
+ export type { MessageListProps } from "./components/MessageList.js";
16
+ export { MessageInput } from "./components/MessageInput.js";
17
+ export type { MessageInputProps, Attachment } from "./components/MessageInput.js";
18
+ export { PartRenderer } from "./parts/PartRenderer.js";
19
+ export type { PartRendererProps } from "./parts/PartRenderer.js";
20
+ export { ReasoningBlock } from "./parts/ReasoningBlock.js";
21
+ export type { ReasoningBlockProps } from "./parts/ReasoningBlock.js";
22
+ export { ToolActivity, defaultToolIconMap } from "./parts/ToolActivity.js";
23
+ export type { ToolActivityProps, ToolActivityState } from "./parts/ToolActivity.js";
24
+ export { ToolResult } from "./parts/ToolResult.js";
25
+ export type { ToolResultProps } from "./parts/ToolResult.js";
26
+ export { AlertRenderer } from "./display/AlertRenderer.js";
27
+ export { MetricCardRenderer } from "./display/MetricCardRenderer.js";
28
+ export { PriceHighlightRenderer } from "./display/PriceHighlightRenderer.js";
29
+ export { FileCardRenderer } from "./display/FileCardRenderer.js";
30
+ export { CodeBlockRenderer } from "./display/CodeBlockRenderer.js";
31
+ export { SourcesListRenderer } from "./display/SourcesListRenderer.js";
32
+ export { StepTimelineRenderer } from "./display/StepTimelineRenderer.js";
33
+ export { ProgressStepsRenderer } from "./display/ProgressStepsRenderer.js";
34
+ export { ChartRenderer } from "./display/ChartRenderer.js";
35
+ export { CarouselRenderer } from "./display/CarouselRenderer.js";
36
+ export { ProductCardRenderer } from "./display/ProductCardRenderer.js";
37
+ export { ComparisonTableRenderer } from "./display/ComparisonTableRenderer.js";
38
+ export { DataTableRenderer } from "./display/DataTableRenderer.js";
39
+ export { SpreadsheetRenderer } from "./display/SpreadsheetRenderer.js";
40
+ export { GalleryRenderer } from "./display/GalleryRenderer.js";
41
+ export { ImageViewerRenderer } from "./display/ImageViewerRenderer.js";
42
+ export { LinkPreviewRenderer } from "./display/LinkPreviewRenderer.js";
43
+ export { MapViewRenderer } from "./display/MapViewRenderer.js";
44
+ export { ChoiceButtonsRenderer } from "./display/ChoiceButtonsRenderer.js";
45
+ export { defaultDisplayRenderers, resolveDisplayRenderer } from "./display/registry.js";
46
+ export type { DisplayRendererMap, DisplayActionName } from "./display/registry.js";
47
+ export { useIsMobile } from "./hooks/useIsMobile.js";
package/dist/index.js ADDED
@@ -0,0 +1,42 @@
1
+ // @codrstudio/openclaude-chat — barrel
2
+ // Componente principal
3
+ export { Chat } from "./components/Chat.js";
4
+ // Hook + Provider
5
+ export { useOpenClaudeChat } from "./hooks/useOpenClaudeChat.js";
6
+ export { ChatProvider, useChatContext } from "./hooks/ChatProvider.js";
7
+ // Subcomponentes
8
+ export { Markdown } from "./components/Markdown.js";
9
+ export { StreamingIndicator } from "./components/StreamingIndicator.js";
10
+ export { ErrorNote } from "./components/ErrorNote.js";
11
+ export { MessageBubble } from "./components/MessageBubble.js";
12
+ export { MessageList } from "./components/MessageList.js";
13
+ export { MessageInput } from "./components/MessageInput.js";
14
+ // Parts
15
+ export { PartRenderer } from "./parts/PartRenderer.js";
16
+ export { ReasoningBlock } from "./parts/ReasoningBlock.js";
17
+ export { ToolActivity, defaultToolIconMap } from "./parts/ToolActivity.js";
18
+ export { ToolResult } from "./parts/ToolResult.js";
19
+ // Display renderers
20
+ export { AlertRenderer } from "./display/AlertRenderer.js";
21
+ export { MetricCardRenderer } from "./display/MetricCardRenderer.js";
22
+ export { PriceHighlightRenderer } from "./display/PriceHighlightRenderer.js";
23
+ export { FileCardRenderer } from "./display/FileCardRenderer.js";
24
+ export { CodeBlockRenderer } from "./display/CodeBlockRenderer.js";
25
+ export { SourcesListRenderer } from "./display/SourcesListRenderer.js";
26
+ export { StepTimelineRenderer } from "./display/StepTimelineRenderer.js";
27
+ export { ProgressStepsRenderer } from "./display/ProgressStepsRenderer.js";
28
+ export { ChartRenderer } from "./display/ChartRenderer.js";
29
+ export { CarouselRenderer } from "./display/CarouselRenderer.js";
30
+ export { ProductCardRenderer } from "./display/ProductCardRenderer.js";
31
+ export { ComparisonTableRenderer } from "./display/ComparisonTableRenderer.js";
32
+ export { DataTableRenderer } from "./display/DataTableRenderer.js";
33
+ export { SpreadsheetRenderer } from "./display/SpreadsheetRenderer.js";
34
+ export { GalleryRenderer } from "./display/GalleryRenderer.js";
35
+ export { ImageViewerRenderer } from "./display/ImageViewerRenderer.js";
36
+ export { LinkPreviewRenderer } from "./display/LinkPreviewRenderer.js";
37
+ export { MapViewRenderer } from "./display/MapViewRenderer.js";
38
+ export { ChoiceButtonsRenderer } from "./display/ChoiceButtonsRenderer.js";
39
+ // Registry
40
+ export { defaultDisplayRenderers, resolveDisplayRenderer } from "./display/registry.js";
41
+ // useIsMobile helper reuse
42
+ export { useIsMobile } from "./hooks/useIsMobile.js";
@@ -0,0 +1,2 @@
1
+ import { type ClassValue } from "clsx";
2
+ export declare function cn(...inputs: ClassValue[]): string;
@@ -0,0 +1,5 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ export function cn(...inputs) {
4
+ return twMerge(clsx(inputs));
5
+ }
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ interface State {
3
+ hasError: boolean;
4
+ message?: string;
5
+ }
6
+ interface Props {
7
+ children: React.ReactNode;
8
+ label?: string;
9
+ }
10
+ /**
11
+ * Isola o crash de um renderer (ex: display widget com input invalido) pra
12
+ * evitar que um unico bloco derrube a arvore React inteira. O chat continua
13
+ * renderizando os outros parts, e no lugar do part quebrado mostra um aviso.
14
+ */
15
+ export declare class PartErrorBoundary extends React.Component<Props, State> {
16
+ state: State;
17
+ static getDerivedStateFromError(error: unknown): State;
18
+ componentDidCatch(error: unknown, info: unknown): void;
19
+ render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode>> | import("react/jsx-runtime").JSX.Element;
20
+ }
21
+ export {};
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { AlertTriangle } from "lucide-react";
4
+ /**
5
+ * Isola o crash de um renderer (ex: display widget com input invalido) pra
6
+ * evitar que um unico bloco derrube a arvore React inteira. O chat continua
7
+ * renderizando os outros parts, e no lugar do part quebrado mostra um aviso.
8
+ */
9
+ export class PartErrorBoundary extends React.Component {
10
+ state = { hasError: false };
11
+ static getDerivedStateFromError(error) {
12
+ return {
13
+ hasError: true,
14
+ message: error instanceof Error ? error.message : String(error),
15
+ };
16
+ }
17
+ componentDidCatch(error, info) {
18
+ // eslint-disable-next-line no-console
19
+ console.warn("[openclaude-chat] part renderer failed:", error, info);
20
+ }
21
+ render() {
22
+ if (this.state.hasError) {
23
+ return (_jsxs("div", { role: "alert", className: "flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive", children: [_jsx(AlertTriangle, { className: "size-3.5 shrink-0 mt-0.5" }), _jsxs("span", { className: "flex-1 min-w-0 break-words font-mono", children: [this.props.label ? `[${this.props.label}] ` : "", this.state.message ?? "Falha ao renderizar bloco"] })] }));
24
+ }
25
+ return this.props.children;
26
+ }
27
+ }
@@ -0,0 +1,8 @@
1
+ import type { DisplayRendererMap } from "../display/registry.js";
2
+ import type { MessagePart } from "../types.js";
3
+ export interface PartRendererProps {
4
+ part: MessagePart;
5
+ isStreaming?: boolean;
6
+ displayRenderers?: DisplayRendererMap;
7
+ }
8
+ export declare const PartRenderer: import("react").NamedExoticComponent<PartRendererProps>;