create-claude-code-visualizer 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.
- package/index.js +393 -0
- package/package.json +31 -0
- package/templates/CLAUDE.md +108 -0
- package/templates/app/.env.local.example +14 -0
- package/templates/app/ecosystem.config.js +29 -0
- package/templates/app/next-env.d.ts +6 -0
- package/templates/app/next.config.ts +16 -0
- package/templates/app/package-lock.json +4581 -0
- package/templates/app/package.json +38 -0
- package/templates/app/postcss.config.js +5 -0
- package/templates/app/src/app/agents/[slug]/chat/loading.tsx +26 -0
- package/templates/app/src/app/agents/[slug]/chat/page.tsx +579 -0
- package/templates/app/src/app/agents/[slug]/loading.tsx +19 -0
- package/templates/app/src/app/agents/page.tsx +8 -0
- package/templates/app/src/app/api/agents/[slug]/capabilities/route.ts +11 -0
- package/templates/app/src/app/api/agents/[slug]/route.ts +57 -0
- package/templates/app/src/app/api/agents/route.ts +28 -0
- package/templates/app/src/app/api/ai/generate-agent/route.ts +87 -0
- package/templates/app/src/app/api/ai/improve-claude-md/route.ts +78 -0
- package/templates/app/src/app/api/ai/suggestions/route.ts +64 -0
- package/templates/app/src/app/api/ai/title/route.ts +88 -0
- package/templates/app/src/app/api/auth/role/route.ts +17 -0
- package/templates/app/src/app/api/commands/[slug]/route.ts +61 -0
- package/templates/app/src/app/api/commands/route.ts +6 -0
- package/templates/app/src/app/api/governance/costs/route.ts +117 -0
- package/templates/app/src/app/api/governance/sessions/route.ts +335 -0
- package/templates/app/src/app/api/notifications/route.ts +62 -0
- package/templates/app/src/app/api/preferences/route.ts +44 -0
- package/templates/app/src/app/api/runs/[id]/approve/route.ts +38 -0
- package/templates/app/src/app/api/runs/[id]/events/route.ts +28 -0
- package/templates/app/src/app/api/runs/[id]/metadata/route.ts +30 -0
- package/templates/app/src/app/api/runs/[id]/route.ts +21 -0
- package/templates/app/src/app/api/runs/[id]/start/route.ts +61 -0
- package/templates/app/src/app/api/runs/[id]/stop/route.ts +16 -0
- package/templates/app/src/app/api/runs/[id]/stream/route.ts +201 -0
- package/templates/app/src/app/api/runs/route.ts +95 -0
- package/templates/app/src/app/api/schedules/[id]/route.ts +81 -0
- package/templates/app/src/app/api/schedules/route.ts +75 -0
- package/templates/app/src/app/api/settings/access-logs/route.ts +33 -0
- package/templates/app/src/app/api/settings/claude-md/route.ts +44 -0
- package/templates/app/src/app/api/settings/env-keys/route.ts +271 -0
- package/templates/app/src/app/api/settings/users/route.ts +108 -0
- package/templates/app/src/app/api/skills/[slug]/route.ts +43 -0
- package/templates/app/src/app/api/skills/route.ts +6 -0
- package/templates/app/src/app/api/tools/route.ts +65 -0
- package/templates/app/src/app/api/uploads/cleanup/route.ts +29 -0
- package/templates/app/src/app/api/uploads/route.ts +77 -0
- package/templates/app/src/app/auth/callback/route.ts +19 -0
- package/templates/app/src/app/globals.css +115 -0
- package/templates/app/src/app/layout.tsx +24 -0
- package/templates/app/src/app/loading.tsx +16 -0
- package/templates/app/src/app/login/page.tsx +64 -0
- package/templates/app/src/app/not-authorized/page.tsx +33 -0
- package/templates/app/src/app/runs/page.tsx +55 -0
- package/templates/app/src/app/schedules/page.tsx +110 -0
- package/templates/app/src/app/settings/page.tsx +1294 -0
- package/templates/app/src/app/skills/page.tsx +7 -0
- package/templates/app/src/components/agent-card.tsx +58 -0
- package/templates/app/src/components/agent-grid.tsx +90 -0
- package/templates/app/src/components/auth/auth-context.tsx +79 -0
- package/templates/app/src/components/chat-thread.tsx +50 -0
- package/templates/app/src/components/chat-view.tsx +670 -0
- package/templates/app/src/components/commands-browser.tsx +349 -0
- package/templates/app/src/components/create-agent-modal.tsx +388 -0
- package/templates/app/src/components/governance-dashboard.tsx +397 -0
- package/templates/app/src/components/icons.tsx +401 -0
- package/templates/app/src/components/layout/agent-sidebar.tsx +504 -0
- package/templates/app/src/components/layout/app-shell.tsx +29 -0
- package/templates/app/src/components/layout/nav.tsx +87 -0
- package/templates/app/src/components/layout/overview-inner.tsx +14 -0
- package/templates/app/src/components/layout/profile-menu.tsx +95 -0
- package/templates/app/src/components/layout/sidebar.tsx +30 -0
- package/templates/app/src/components/markdown.tsx +57 -0
- package/templates/app/src/components/message-bar.tsx +161 -0
- package/templates/app/src/components/notifications/notification-bell.tsx +104 -0
- package/templates/app/src/components/notifications/notification-panel.tsx +116 -0
- package/templates/app/src/components/overview/overview-content.tsx +287 -0
- package/templates/app/src/components/overview/overview-context.tsx +88 -0
- package/templates/app/src/components/preferences-modal.tsx +112 -0
- package/templates/app/src/components/run-form.tsx +73 -0
- package/templates/app/src/components/run-history-table.tsx +226 -0
- package/templates/app/src/components/run-output.tsx +187 -0
- package/templates/app/src/components/schedule-form.tsx +148 -0
- package/templates/app/src/components/skills-browser.tsx +338 -0
- package/templates/app/src/components/tool-tooltip.tsx +82 -0
- package/templates/app/src/hooks/use-sse.ts +115 -0
- package/templates/app/src/instrumentation.ts +9 -0
- package/templates/app/src/lib/agent-cache.ts +19 -0
- package/templates/app/src/lib/agent-runner.ts +411 -0
- package/templates/app/src/lib/agents.ts +168 -0
- package/templates/app/src/lib/ai.ts +40 -0
- package/templates/app/src/lib/approval-store.ts +70 -0
- package/templates/app/src/lib/auth-guard.ts +116 -0
- package/templates/app/src/lib/capabilities.ts +191 -0
- package/templates/app/src/lib/line-diff.ts +96 -0
- package/templates/app/src/lib/queue.ts +22 -0
- package/templates/app/src/lib/redis.ts +12 -0
- package/templates/app/src/lib/role-permissions.ts +166 -0
- package/templates/app/src/lib/run-agent.ts +442 -0
- package/templates/app/src/lib/supabase-browser.ts +8 -0
- package/templates/app/src/lib/supabase-middleware.ts +63 -0
- package/templates/app/src/lib/supabase-server.ts +28 -0
- package/templates/app/src/lib/supabase.ts +6 -0
- package/templates/app/src/lib/tool-descriptions.ts +29 -0
- package/templates/app/src/lib/types.ts +73 -0
- package/templates/app/src/lib/typewriter-animation.ts +159 -0
- package/templates/app/src/middleware.ts +13 -0
- package/templates/app/tsconfig.json +21 -0
- package/templates/app/uploads/.gitkeep +0 -0
- package/templates/app/worker/index.ts +342 -0
- package/templates/claude/agents/ai-trends-scout.md +66 -0
- package/templates/claude/commands/add-to-todos.md +56 -0
- package/templates/claude/commands/check-todos.md +56 -0
- package/templates/claude/hooks/auto-approve-safe.sh +34 -0
- package/templates/claude/hooks/auto-format.sh +25 -0
- package/templates/claude/hooks/block-destructive.sh +32 -0
- package/templates/claude/hooks/compaction-preserver.sh +16 -0
- package/templates/claude/hooks/notify.sh +26 -0
- package/templates/claude/settings.local.json +66 -0
- package/templates/claude/skills/frontend-design/SKILL.md +127 -0
- package/templates/claude/skills/frontend-design/reference/color-and-contrast.md +132 -0
- package/templates/claude/skills/frontend-design/reference/interaction-design.md +123 -0
- package/templates/claude/skills/frontend-design/reference/motion-design.md +99 -0
- package/templates/claude/skills/frontend-design/reference/responsive-design.md +114 -0
- package/templates/claude/skills/frontend-design/reference/spatial-design.md +100 -0
- package/templates/claude/skills/frontend-design/reference/typography.md +131 -0
- package/templates/claude/skills/frontend-design/reference/ux-writing.md +107 -0
- package/templates/claude/skills/gws-admin-reports/SKILL.md +57 -0
- package/templates/claude/skills/gws-calendar/SKILL.md +108 -0
- package/templates/claude/skills/gws-calendar-agenda/SKILL.md +52 -0
- package/templates/claude/skills/gws-calendar-insert/SKILL.md +55 -0
- package/templates/claude/skills/gws-chat/SKILL.md +73 -0
- package/templates/claude/skills/gws-chat-send/SKILL.md +49 -0
- package/templates/claude/skills/gws-classroom/SKILL.md +75 -0
- package/templates/claude/skills/gws-docs/SKILL.md +48 -0
- package/templates/claude/skills/gws-docs-write/SKILL.md +49 -0
- package/templates/claude/skills/gws-drive/SKILL.md +137 -0
- package/templates/claude/skills/gws-drive-upload/SKILL.md +52 -0
- package/templates/claude/skills/gws-events/SKILL.md +67 -0
- package/templates/claude/skills/gws-events-renew/SKILL.md +48 -0
- package/templates/claude/skills/gws-events-subscribe/SKILL.md +59 -0
- package/templates/claude/skills/gws-forms/SKILL.md +45 -0
- package/templates/claude/skills/gws-gmail/SKILL.md +59 -0
- package/templates/claude/skills/gws-gmail-forward/SKILL.md +53 -0
- package/templates/claude/skills/gws-gmail-reply/SKILL.md +56 -0
- package/templates/claude/skills/gws-gmail-reply-all/SKILL.md +60 -0
- package/templates/claude/skills/gws-gmail-send/SKILL.md +55 -0
- package/templates/claude/skills/gws-gmail-triage/SKILL.md +50 -0
- package/templates/claude/skills/gws-gmail-watch/SKILL.md +58 -0
- package/templates/claude/skills/gws-keep/SKILL.md +48 -0
- package/templates/claude/skills/gws-meet/SKILL.md +51 -0
- package/templates/claude/skills/gws-modelarmor/SKILL.md +42 -0
- package/templates/claude/skills/gws-modelarmor-create-template/SKILL.md +53 -0
- package/templates/claude/skills/gws-modelarmor-sanitize-prompt/SKILL.md +48 -0
- package/templates/claude/skills/gws-modelarmor-sanitize-response/SKILL.md +48 -0
- package/templates/claude/skills/gws-people/SKILL.md +67 -0
- package/templates/claude/skills/gws-shared/SKILL.md +66 -0
- package/templates/claude/skills/gws-sheets/SKILL.md +53 -0
- package/templates/claude/skills/gws-sheets-append/SKILL.md +51 -0
- package/templates/claude/skills/gws-sheets-read/SKILL.md +47 -0
- package/templates/claude/skills/gws-slides/SKILL.md +43 -0
- package/templates/claude/skills/gws-tasks/SKILL.md +56 -0
- package/templates/claude/skills/gws-workflow/SKILL.md +44 -0
- package/templates/claude/skills/gws-workflow-email-to-task/SKILL.md +47 -0
- package/templates/claude/skills/gws-workflow-file-announce/SKILL.md +50 -0
- package/templates/claude/skills/gws-workflow-meeting-prep/SKILL.md +47 -0
- package/templates/claude/skills/gws-workflow-standup-report/SKILL.md +46 -0
- package/templates/claude/skills/gws-workflow-weekly-digest/SKILL.md +46 -0
- package/templates/claude/skills/persona-content-creator/SKILL.md +33 -0
- package/templates/claude/skills/persona-customer-support/SKILL.md +34 -0
- package/templates/claude/skills/persona-event-coordinator/SKILL.md +35 -0
- package/templates/claude/skills/persona-exec-assistant/SKILL.md +35 -0
- package/templates/claude/skills/persona-hr-coordinator/SKILL.md +33 -0
- package/templates/claude/skills/persona-it-admin/SKILL.md +30 -0
- package/templates/claude/skills/persona-project-manager/SKILL.md +35 -0
- package/templates/claude/skills/persona-researcher/SKILL.md +33 -0
- package/templates/claude/skills/persona-sales-ops/SKILL.md +35 -0
- package/templates/claude/skills/persona-team-lead/SKILL.md +36 -0
- package/templates/claude/skills/recipe-backup-sheet-as-csv/SKILL.md +25 -0
- package/templates/claude/skills/recipe-batch-invite-to-event/SKILL.md +25 -0
- package/templates/claude/skills/recipe-block-focus-time/SKILL.md +24 -0
- package/templates/claude/skills/recipe-bulk-download-folder/SKILL.md +25 -0
- package/templates/claude/skills/recipe-collect-form-responses/SKILL.md +25 -0
- package/templates/claude/skills/recipe-compare-sheet-tabs/SKILL.md +25 -0
- package/templates/claude/skills/recipe-copy-sheet-for-new-month/SKILL.md +25 -0
- package/templates/claude/skills/recipe-create-classroom-course/SKILL.md +25 -0
- package/templates/claude/skills/recipe-create-doc-from-template/SKILL.md +29 -0
- package/templates/claude/skills/recipe-create-events-from-sheet/SKILL.md +24 -0
- package/templates/claude/skills/recipe-create-expense-tracker/SKILL.md +26 -0
- package/templates/claude/skills/recipe-create-feedback-form/SKILL.md +25 -0
- package/templates/claude/skills/recipe-create-gmail-filter/SKILL.md +26 -0
- package/templates/claude/skills/recipe-create-meet-space/SKILL.md +25 -0
- package/templates/claude/skills/recipe-create-presentation/SKILL.md +25 -0
- package/templates/claude/skills/recipe-create-shared-drive/SKILL.md +25 -0
- package/templates/claude/skills/recipe-create-task-list/SKILL.md +26 -0
- package/templates/claude/skills/recipe-create-vacation-responder/SKILL.md +25 -0
- package/templates/claude/skills/recipe-draft-email-from-doc/SKILL.md +25 -0
- package/templates/claude/skills/recipe-email-drive-link/SKILL.md +25 -0
- package/templates/claude/skills/recipe-find-free-time/SKILL.md +25 -0
- package/templates/claude/skills/recipe-find-large-files/SKILL.md +24 -0
- package/templates/claude/skills/recipe-forward-labeled-emails/SKILL.md +27 -0
- package/templates/claude/skills/recipe-generate-report-from-sheet/SKILL.md +34 -0
- package/templates/claude/skills/recipe-label-and-archive-emails/SKILL.md +25 -0
- package/templates/claude/skills/recipe-log-deal-update/SKILL.md +25 -0
- package/templates/claude/skills/recipe-organize-drive-folder/SKILL.md +26 -0
- package/templates/claude/skills/recipe-plan-weekly-schedule/SKILL.md +26 -0
- package/templates/claude/skills/recipe-post-mortem-setup/SKILL.md +25 -0
- package/templates/claude/skills/recipe-reschedule-meeting/SKILL.md +25 -0
- package/templates/claude/skills/recipe-review-meet-participants/SKILL.md +25 -0
- package/templates/claude/skills/recipe-review-overdue-tasks/SKILL.md +25 -0
- package/templates/claude/skills/recipe-save-email-attachments/SKILL.md +26 -0
- package/templates/claude/skills/recipe-save-email-to-doc/SKILL.md +29 -0
- package/templates/claude/skills/recipe-schedule-recurring-event/SKILL.md +24 -0
- package/templates/claude/skills/recipe-send-team-announcement/SKILL.md +24 -0
- package/templates/claude/skills/recipe-share-doc-and-notify/SKILL.md +25 -0
- package/templates/claude/skills/recipe-share-event-materials/SKILL.md +25 -0
- package/templates/claude/skills/recipe-share-folder-with-team/SKILL.md +26 -0
- package/templates/claude/skills/recipe-sync-contacts-to-sheet/SKILL.md +25 -0
- package/templates/claude/skills/recipe-watch-drive-changes/SKILL.md +25 -0
- package/templates/mcp.json +12 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import type { SSEMessage } from "@/hooks/use-sse";
|
|
6
|
+
import type { AgentRun } from "@/lib/types";
|
|
7
|
+
import { Markdown } from "@/components/markdown";
|
|
8
|
+
import { ToolBadge } from "@/components/tool-tooltip";
|
|
9
|
+
import { XIcon, CheckIcon, ToolIcon, ChevronRightIcon } from "@/components/icons";
|
|
10
|
+
|
|
11
|
+
// --- Types ---
|
|
12
|
+
|
|
13
|
+
interface ApprovalData {
|
|
14
|
+
tool_name: string;
|
|
15
|
+
tool_input: Record<string, unknown>;
|
|
16
|
+
tool_use_id: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ChatViewProps {
|
|
20
|
+
conversationRuns: AgentRun[];
|
|
21
|
+
completedRunEvents?: Record<string, SSEMessage[]>;
|
|
22
|
+
messages: SSEMessage[];
|
|
23
|
+
isConnected: boolean;
|
|
24
|
+
isRunning: boolean;
|
|
25
|
+
currentPrompt: string | null;
|
|
26
|
+
onApprove?: (toolUseId: string, approved: boolean) => void;
|
|
27
|
+
loading?: boolean;
|
|
28
|
+
suggestions?: string[];
|
|
29
|
+
onSuggestionClick?: (text: string) => void;
|
|
30
|
+
agentName?: string;
|
|
31
|
+
agentEmoji?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ToolEntry {
|
|
35
|
+
name: string;
|
|
36
|
+
done: boolean;
|
|
37
|
+
input?: unknown;
|
|
38
|
+
result?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Segment =
|
|
42
|
+
| { kind: "thinking" }
|
|
43
|
+
| { kind: "tools"; tools: ToolEntry[]; startIndex: number }
|
|
44
|
+
| { kind: "text"; text: string }
|
|
45
|
+
| { kind: "approval"; data: ApprovalData }
|
|
46
|
+
| { kind: "error"; message: string };
|
|
47
|
+
|
|
48
|
+
// --- Helpers ---
|
|
49
|
+
|
|
50
|
+
function formatToolName(name: string): string {
|
|
51
|
+
if (!name || name === "unknown") return name;
|
|
52
|
+
if (!name.startsWith("mcp__")) return name;
|
|
53
|
+
const parts = name.replace(/^mcp__/, "").split("__");
|
|
54
|
+
const service = (parts[0] || "").replace(/^claude_ai_/, "");
|
|
55
|
+
const action = (parts[1] || "").replace(/^[^_]+_/, "").replace(/_/g, " ");
|
|
56
|
+
return `${service} / ${action}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatDuration(ms: number): string {
|
|
60
|
+
return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isCreditError(msg: string): boolean {
|
|
64
|
+
const lower = msg.toLowerCase();
|
|
65
|
+
return lower.includes("credit balance") || lower.includes("insufficient") || lower.includes("billing");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildSegments(messages: SSEMessage[], handledApprovals: Set<string>): Segment[] {
|
|
69
|
+
const segments: Segment[] = [];
|
|
70
|
+
let currentText = "";
|
|
71
|
+
let currentToolSegment: { kind: "tools"; tools: ToolEntry[]; startIndex: number } | null = null;
|
|
72
|
+
let isThinking = false;
|
|
73
|
+
let creditErrorEmitted = false;
|
|
74
|
+
|
|
75
|
+
const flushText = () => {
|
|
76
|
+
if (currentText) {
|
|
77
|
+
segments.push({ kind: "text", text: currentText });
|
|
78
|
+
currentText = "";
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const flushTools = () => {
|
|
83
|
+
if (currentToolSegment) {
|
|
84
|
+
segments.push(currentToolSegment);
|
|
85
|
+
currentToolSegment = null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < messages.length; i++) {
|
|
90
|
+
const m = messages[i];
|
|
91
|
+
|
|
92
|
+
if (m.type === "thinking") {
|
|
93
|
+
if (m.active) {
|
|
94
|
+
flushText();
|
|
95
|
+
flushTools();
|
|
96
|
+
if (!isThinking) {
|
|
97
|
+
segments.push({ kind: "thinking" });
|
|
98
|
+
isThinking = true;
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
isThinking = false;
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (m.type === "tool_call" && m.status === "start") {
|
|
107
|
+
isThinking = false;
|
|
108
|
+
flushText();
|
|
109
|
+
if (!currentToolSegment) {
|
|
110
|
+
currentToolSegment = { kind: "tools", tools: [], startIndex: segments.length };
|
|
111
|
+
}
|
|
112
|
+
const toolName = (m.name as string) || "unknown";
|
|
113
|
+
currentToolSegment.tools.push({ name: toolName, done: false, input: m.input as unknown });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (m.type === "tool_call" && m.status === "end") {
|
|
118
|
+
if (currentToolSegment && currentToolSegment.tools.length > 0) {
|
|
119
|
+
const undone = currentToolSegment.tools.find((t) => !t.done);
|
|
120
|
+
if (undone) {
|
|
121
|
+
undone.done = true;
|
|
122
|
+
if (m.result) undone.result = m.result as string;
|
|
123
|
+
if (!undone.input && m.input) undone.input = m.input as unknown;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (m.type === "token" || m.type === "text_chunk") {
|
|
130
|
+
isThinking = false;
|
|
131
|
+
flushTools();
|
|
132
|
+
currentText += m.text as string;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (m.type === "approval_required" && !handledApprovals.has(m.tool_use_id as string)) {
|
|
137
|
+
flushText();
|
|
138
|
+
flushTools();
|
|
139
|
+
segments.push({
|
|
140
|
+
kind: "approval",
|
|
141
|
+
data: {
|
|
142
|
+
tool_name: m.tool_name as string,
|
|
143
|
+
tool_input: m.tool_input as Record<string, unknown>,
|
|
144
|
+
tool_use_id: m.tool_use_id as string,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (m.type === "error" && !messages.some((d) => d.type === "done" || d.type === "stopped")) {
|
|
151
|
+
const errMsg = m.error as string;
|
|
152
|
+
// Deduplicate credit errors — only show one CreditErrorCard per run
|
|
153
|
+
if (isCreditError(errMsg)) {
|
|
154
|
+
if (creditErrorEmitted) continue;
|
|
155
|
+
creditErrorEmitted = true;
|
|
156
|
+
}
|
|
157
|
+
flushText();
|
|
158
|
+
flushTools();
|
|
159
|
+
segments.push({ kind: "error", message: errMsg });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
flushTools();
|
|
165
|
+
flushText();
|
|
166
|
+
return segments;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function countTools(messages: SSEMessage[]): number {
|
|
170
|
+
return messages.filter((m) => m.type === "tool_call" && m.status === "start").length;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getLiveState(messages: SSEMessage[]): "thinking" | "tool" | "streaming" | "idle" {
|
|
174
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
175
|
+
const m = messages[i];
|
|
176
|
+
if (m.type === "done" || m.type === "stopped" || m.type === "error") return "idle";
|
|
177
|
+
if (m.type === "thinking" && m.active) return "thinking";
|
|
178
|
+
if (m.type === "tool_call" && m.status === "start") return "tool";
|
|
179
|
+
if (m.type === "token" || m.type === "text_chunk") return "streaming";
|
|
180
|
+
if (m.type === "tool_call" && m.status === "end") return "idle";
|
|
181
|
+
if (m.type === "thinking" && !m.active) return "idle";
|
|
182
|
+
}
|
|
183
|
+
return "idle";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- Sub-components ---
|
|
187
|
+
|
|
188
|
+
/** User message — right-aligned, subtle background, no heavy bubble */
|
|
189
|
+
function UserMessage({ children, meta }: { children: React.ReactNode; meta?: React.ReactNode }) {
|
|
190
|
+
return (
|
|
191
|
+
<div className="flex justify-end mb-5">
|
|
192
|
+
<div className="max-w-[75%] min-w-[60px]">
|
|
193
|
+
<div className="rounded-lg bg-[var(--bg-elevated)] px-4 py-2.5">
|
|
194
|
+
<div className="text-[13px] leading-relaxed text-[var(--text-primary)] whitespace-pre-wrap">{children}</div>
|
|
195
|
+
</div>
|
|
196
|
+
{meta && <div className="mt-1 text-right">{meta}</div>}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Agent response — left-aligned, no container/bubble, just content */
|
|
203
|
+
function AgentResponse({
|
|
204
|
+
children,
|
|
205
|
+
meta,
|
|
206
|
+
}: {
|
|
207
|
+
children: React.ReactNode;
|
|
208
|
+
live?: boolean;
|
|
209
|
+
meta?: React.ReactNode;
|
|
210
|
+
}) {
|
|
211
|
+
return (
|
|
212
|
+
<div className="mb-5">
|
|
213
|
+
<div className="max-w-[90%]">
|
|
214
|
+
{children}
|
|
215
|
+
</div>
|
|
216
|
+
{meta && <div className="mt-1">{meta}</div>}
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Tool result detail panel */
|
|
222
|
+
function ToolResultPopup({ tool, onClose }: { tool: ToolEntry; onClose: () => void }) {
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
|
225
|
+
window.addEventListener("keydown", handler);
|
|
226
|
+
return () => window.removeEventListener("keydown", handler);
|
|
227
|
+
}, [onClose]);
|
|
228
|
+
|
|
229
|
+
const hasInput = tool.input != null && Object.keys(tool.input as Record<string, unknown>).length > 0;
|
|
230
|
+
const hasResult = !!tool.result;
|
|
231
|
+
|
|
232
|
+
const formatInput = (input: unknown): string => {
|
|
233
|
+
if (!input) return "";
|
|
234
|
+
if (typeof input === "string") return input;
|
|
235
|
+
try { return JSON.stringify(input, null, 2); } catch { return String(input); }
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
return createPortal(
|
|
239
|
+
<div
|
|
240
|
+
className="fixed inset-0 z-[10000] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
241
|
+
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
|
242
|
+
>
|
|
243
|
+
<div className="relative w-full max-w-2xl max-h-[80vh] mx-4 rounded-lg bg-[#111113] border border-[var(--border-default)] shadow-2xl flex flex-col overflow-hidden">
|
|
244
|
+
{/* Header */}
|
|
245
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border-subtle)]">
|
|
246
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
247
|
+
<ToolIcon size={14} className="text-[var(--text-muted)] shrink-0" />
|
|
248
|
+
<span className="text-[13px] font-mono text-[var(--text-primary)] truncate">{formatToolName(tool.name)}</span>
|
|
249
|
+
</div>
|
|
250
|
+
<button onClick={onClose} className="text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors p-1 -mr-1">
|
|
251
|
+
<XIcon size={14} />
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* Body */}
|
|
256
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
257
|
+
{hasInput && (
|
|
258
|
+
<div>
|
|
259
|
+
<div className="text-[10px] font-medium text-[var(--text-muted)] uppercase tracking-wider mb-1.5">Input</div>
|
|
260
|
+
<pre className="rounded-md bg-[var(--bg-base)] border border-[var(--border-subtle)] p-3 text-[12px] text-[var(--text-tertiary)] font-mono whitespace-pre-wrap break-words overflow-x-auto max-h-48 overflow-y-auto">
|
|
261
|
+
{formatInput(tool.input)}
|
|
262
|
+
</pre>
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
{hasResult ? (
|
|
266
|
+
<div>
|
|
267
|
+
<div className="text-[10px] font-medium text-[var(--text-muted)] uppercase tracking-wider mb-1.5">Result</div>
|
|
268
|
+
<pre className="rounded-md bg-[var(--bg-base)] border border-[var(--border-subtle)] p-3 text-[12px] text-[var(--text-tertiary)] font-mono whitespace-pre-wrap break-words overflow-x-auto max-h-96 overflow-y-auto">
|
|
269
|
+
{tool.result}
|
|
270
|
+
</pre>
|
|
271
|
+
</div>
|
|
272
|
+
) : !hasInput ? (
|
|
273
|
+
<p className="text-[12px] text-[var(--text-muted)] italic">No data available for this tool call.</p>
|
|
274
|
+
) : null}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>,
|
|
278
|
+
document.body
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Tool call list — compact, inline */
|
|
283
|
+
function ToolSegmentBlock({ tools, isLive }: { tools: ToolEntry[]; isLive: boolean }) {
|
|
284
|
+
const activeTool = tools.find((t) => !t.done);
|
|
285
|
+
const [selectedTool, setSelectedTool] = useState<ToolEntry | null>(null);
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<>
|
|
289
|
+
<div className="mb-3 pl-3 border-l-2 border-[var(--border-subtle)] space-y-0.5">
|
|
290
|
+
{tools.map((tool, i) => {
|
|
291
|
+
const hasDetail = !!(tool.done && (tool.result || tool.input));
|
|
292
|
+
return (
|
|
293
|
+
<div
|
|
294
|
+
key={i}
|
|
295
|
+
className={`flex items-center gap-2 py-0.5 ${hasDetail ? "cursor-pointer group/tool" : ""}`}
|
|
296
|
+
onClick={hasDetail ? () => setSelectedTool(tool) : undefined}
|
|
297
|
+
>
|
|
298
|
+
{tool.done ? (
|
|
299
|
+
<CheckIcon size={12} className="text-green-400/60 shrink-0" />
|
|
300
|
+
) : (
|
|
301
|
+
<span className="relative flex h-3 w-3 shrink-0 items-center justify-center">
|
|
302
|
+
<span className="absolute h-2 w-2 animate-ping rounded-full bg-[var(--accent)] opacity-30" />
|
|
303
|
+
<span className="relative h-1.5 w-1.5 rounded-full bg-[var(--accent)]" />
|
|
304
|
+
</span>
|
|
305
|
+
)}
|
|
306
|
+
<ToolBadge
|
|
307
|
+
name={tool.name}
|
|
308
|
+
className={`text-[12px] font-mono ${hasDetail ? "cursor-pointer" : "cursor-help"} ${tool.done ? "text-[var(--text-muted)] group-hover/tool:text-[var(--text-tertiary)]" : "text-[var(--text-secondary)]"} transition-colors`}
|
|
309
|
+
>
|
|
310
|
+
{formatToolName(tool.name)}
|
|
311
|
+
</ToolBadge>
|
|
312
|
+
{hasDetail && (
|
|
313
|
+
<ChevronRightIcon size={10} className="text-[var(--text-muted)] opacity-0 group-hover/tool:opacity-100 transition-opacity shrink-0" />
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
})}
|
|
318
|
+
{isLive && activeTool && (
|
|
319
|
+
<span className="text-[10px] text-[var(--text-muted)] pl-5">Running...</span>
|
|
320
|
+
)}
|
|
321
|
+
</div>
|
|
322
|
+
{selectedTool && <ToolResultPopup tool={selectedTool} onClose={() => setSelectedTool(null)} />}
|
|
323
|
+
</>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function ThinkingIndicator() {
|
|
328
|
+
return (
|
|
329
|
+
<div className="mb-3 flex items-center gap-2 pl-3">
|
|
330
|
+
<div className="flex gap-0.5">
|
|
331
|
+
<span className="h-1 w-1 rounded-full bg-[var(--text-muted)] animate-bounce" style={{ animationDelay: "0ms" }} />
|
|
332
|
+
<span className="h-1 w-1 rounded-full bg-[var(--text-muted)] animate-bounce" style={{ animationDelay: "150ms" }} />
|
|
333
|
+
<span className="h-1 w-1 rounded-full bg-[var(--text-muted)] animate-bounce" style={{ animationDelay: "300ms" }} />
|
|
334
|
+
</div>
|
|
335
|
+
<span className="text-[11px] text-[var(--text-muted)]">Thinking</span>
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function RunMeta({ toolCount, costUsd, durationMs }: { toolCount: number; costUsd?: number | null; durationMs?: number | null }) {
|
|
341
|
+
if (toolCount === 0 && costUsd == null && durationMs == null) return null;
|
|
342
|
+
return (
|
|
343
|
+
<div className="flex items-center gap-2 mb-5 text-[10px] text-[var(--text-muted)] tabular-nums">
|
|
344
|
+
{toolCount > 0 && (
|
|
345
|
+
<span className="flex items-center gap-1">
|
|
346
|
+
<ToolIcon size={10} className="opacity-50" />
|
|
347
|
+
{toolCount} tool{toolCount !== 1 ? "s" : ""}
|
|
348
|
+
</span>
|
|
349
|
+
)}
|
|
350
|
+
{costUsd != null && <span>${costUsd.toFixed(4)}</span>}
|
|
351
|
+
{durationMs != null && <span>{formatDuration(durationMs)}</span>}
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function ApprovalCard({ data, onApprove }: { data: ApprovalData; onApprove: (toolUseId: string, approved: boolean) => void }) {
|
|
357
|
+
const [expanded, setExpanded] = useState(false);
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<div className="mb-4 rounded-md border border-amber-500/15 bg-amber-500/5 px-4 py-3 space-y-2.5 max-w-[90%]">
|
|
361
|
+
<div className="flex items-center gap-2">
|
|
362
|
+
<span className="relative flex h-2 w-2">
|
|
363
|
+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-amber-400 opacity-40" />
|
|
364
|
+
<span className="relative inline-flex h-2 w-2 rounded-full bg-amber-400" />
|
|
365
|
+
</span>
|
|
366
|
+
<span className="text-[12px] text-amber-300/90 font-medium">Approval needed</span>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
<p className="text-[13px] text-[var(--text-secondary)]">
|
|
370
|
+
Run <ToolBadge name={data.tool_name} className="font-mono text-amber-200/90 text-[12px] cursor-help">{formatToolName(data.tool_name)}</ToolBadge>?
|
|
371
|
+
</p>
|
|
372
|
+
|
|
373
|
+
{Object.keys(data.tool_input).length > 0 && (
|
|
374
|
+
<div>
|
|
375
|
+
<button
|
|
376
|
+
onClick={() => setExpanded(!expanded)}
|
|
377
|
+
className="text-[11px] text-[var(--text-muted)] hover:text-[var(--text-tertiary)] transition-colors"
|
|
378
|
+
>
|
|
379
|
+
{expanded ? "Hide" : "Show"} parameters
|
|
380
|
+
</button>
|
|
381
|
+
{expanded && (
|
|
382
|
+
<pre className="mt-1 max-h-32 overflow-auto rounded-md border border-[var(--border-subtle)] bg-[var(--bg-base)] p-2 text-[11px] text-[var(--text-muted)] font-mono">
|
|
383
|
+
{JSON.stringify(data.tool_input, null, 2)}
|
|
384
|
+
</pre>
|
|
385
|
+
)}
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
|
|
389
|
+
<div className="flex gap-2">
|
|
390
|
+
<button
|
|
391
|
+
onClick={() => onApprove(data.tool_use_id, true)}
|
|
392
|
+
className="rounded-md bg-green-600/80 px-3 py-1 text-[12px] font-medium text-white transition-colors hover:bg-green-600"
|
|
393
|
+
>
|
|
394
|
+
Allow
|
|
395
|
+
</button>
|
|
396
|
+
<button
|
|
397
|
+
onClick={() => onApprove(data.tool_use_id, false)}
|
|
398
|
+
className="rounded-md border border-[var(--border-default)] px-3 py-1 text-[12px] font-medium text-[var(--text-tertiary)] transition-colors hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
|
|
399
|
+
>
|
|
400
|
+
Deny
|
|
401
|
+
</button>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function CreditErrorCard() {
|
|
408
|
+
return (
|
|
409
|
+
<div className="space-y-2.5">
|
|
410
|
+
<div className="flex items-center gap-2">
|
|
411
|
+
<span className="h-2 w-2 rounded-full bg-red-400" />
|
|
412
|
+
<span className="text-[13px] font-medium text-red-400">API key stuck — credit balance not recognized</span>
|
|
413
|
+
</div>
|
|
414
|
+
<p className="text-[13px] text-[var(--text-secondary)] leading-relaxed">
|
|
415
|
+
Anthropic caches credit balance per API key. Even after credits reload, the old key can stay stuck.
|
|
416
|
+
Generate a new key and paste it in Settings to fix this instantly.
|
|
417
|
+
</p>
|
|
418
|
+
<div className="flex items-center gap-2">
|
|
419
|
+
<a
|
|
420
|
+
href="https://console.anthropic.com/settings/keys"
|
|
421
|
+
target="_blank"
|
|
422
|
+
rel="noopener noreferrer"
|
|
423
|
+
className="inline-flex items-center gap-1.5 rounded-md bg-[var(--accent)] px-3 py-1.5 text-[12px] font-medium text-white transition-colors hover:bg-[var(--accent-hover)]"
|
|
424
|
+
>
|
|
425
|
+
Generate new key
|
|
426
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className="opacity-70">
|
|
427
|
+
<path d="M4.5 2.5H9.5V7.5M9.5 2.5L2.5 9.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
428
|
+
</svg>
|
|
429
|
+
</a>
|
|
430
|
+
<a
|
|
431
|
+
href="/settings"
|
|
432
|
+
className="inline-flex items-center gap-1.5 rounded-md border border-[var(--border-default)] px-3 py-1.5 text-[12px] font-medium text-[var(--text-tertiary)] transition-colors hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
|
|
433
|
+
>
|
|
434
|
+
Paste in Settings
|
|
435
|
+
</a>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// --- Main Component ---
|
|
442
|
+
|
|
443
|
+
export function ChatView({
|
|
444
|
+
conversationRuns,
|
|
445
|
+
completedRunEvents,
|
|
446
|
+
messages,
|
|
447
|
+
isConnected,
|
|
448
|
+
isRunning,
|
|
449
|
+
currentPrompt,
|
|
450
|
+
onApprove,
|
|
451
|
+
loading,
|
|
452
|
+
suggestions,
|
|
453
|
+
onSuggestionClick,
|
|
454
|
+
agentName,
|
|
455
|
+
agentEmoji,
|
|
456
|
+
}: ChatViewProps) {
|
|
457
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
458
|
+
const [handledApprovals, setHandledApprovals] = useState<Set<string>>(new Set());
|
|
459
|
+
|
|
460
|
+
useEffect(() => {
|
|
461
|
+
if (scrollRef.current) {
|
|
462
|
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
463
|
+
}
|
|
464
|
+
}, [messages, conversationRuns]);
|
|
465
|
+
|
|
466
|
+
const segments = useMemo(() => buildSegments(messages, handledApprovals), [messages, handledApprovals]);
|
|
467
|
+
const toolCount = useMemo(() => countTools(messages), [messages]);
|
|
468
|
+
const liveState = useMemo(() => getLiveState(messages), [messages]);
|
|
469
|
+
const doneMsg = messages.find((m) => m.type === "done" || m.type === "stopped");
|
|
470
|
+
|
|
471
|
+
const handleApproval = (toolUseId: string, approved: boolean) => {
|
|
472
|
+
setHandledApprovals((prev) => new Set(prev).add(toolUseId));
|
|
473
|
+
onApprove?.(toolUseId, approved);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const hasContent = conversationRuns.length > 0 || isRunning || messages.length > 0;
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
<div ref={scrollRef} className="flex-1 overflow-y-auto">
|
|
480
|
+
<div className="max-w-3xl mx-auto px-4 py-6">
|
|
481
|
+
{/* Loading skeleton */}
|
|
482
|
+
{loading && !hasContent && (
|
|
483
|
+
<div className="space-y-4 animate-pulse pt-2">
|
|
484
|
+
<div className="flex justify-end"><div className="h-8 w-48 rounded-lg bg-[var(--bg-elevated)]" /></div>
|
|
485
|
+
<div className="h-16 w-72 rounded-lg bg-[var(--bg-raised)]" />
|
|
486
|
+
<div className="flex justify-end"><div className="h-8 w-36 rounded-lg bg-[var(--bg-elevated)]" /></div>
|
|
487
|
+
<div className="h-12 w-64 rounded-lg bg-[var(--bg-raised)]" />
|
|
488
|
+
</div>
|
|
489
|
+
)}
|
|
490
|
+
|
|
491
|
+
{/* Empty state */}
|
|
492
|
+
{!loading && !hasContent && (
|
|
493
|
+
<div className="flex flex-col items-center justify-center h-[50vh] gap-4">
|
|
494
|
+
{agentEmoji && <span className="text-3xl">{agentEmoji}</span>}
|
|
495
|
+
<div className="text-center">
|
|
496
|
+
<h2 className="text-[15px] font-semibold text-[var(--text-primary)]">
|
|
497
|
+
{agentName ? `What can I help with?` : "Start a conversation"}
|
|
498
|
+
</h2>
|
|
499
|
+
{agentName && (
|
|
500
|
+
<p className="text-[12px] text-[var(--text-muted)] mt-1">
|
|
501
|
+
Ask {agentName} anything to get started
|
|
502
|
+
</p>
|
|
503
|
+
)}
|
|
504
|
+
</div>
|
|
505
|
+
{suggestions && suggestions.length > 0 && (
|
|
506
|
+
<div className="flex flex-col gap-1.5 w-full max-w-sm mt-2">
|
|
507
|
+
{suggestions.slice(0, 3).map((s, i) => (
|
|
508
|
+
<button
|
|
509
|
+
key={i}
|
|
510
|
+
onClick={() => onSuggestionClick?.(s)}
|
|
511
|
+
className="text-left px-3 py-2.5 rounded-md border border-[var(--border-subtle)] text-[13px] text-[var(--text-tertiary)] leading-snug transition-colors hover:border-[var(--border-default)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
|
|
512
|
+
>
|
|
513
|
+
{s}
|
|
514
|
+
</button>
|
|
515
|
+
))}
|
|
516
|
+
</div>
|
|
517
|
+
)}
|
|
518
|
+
</div>
|
|
519
|
+
)}
|
|
520
|
+
|
|
521
|
+
{/* Completed conversation turns */}
|
|
522
|
+
{conversationRuns.map((run) => {
|
|
523
|
+
const runEvents = completedRunEvents?.[run.id];
|
|
524
|
+
const meta = run.metadata as Record<string, unknown> | undefined;
|
|
525
|
+
const storedToolCount = meta?.tool_count as number | undefined;
|
|
526
|
+
|
|
527
|
+
if (runEvents && runEvents.length > 0) {
|
|
528
|
+
const completedSegments = buildSegments(runEvents, handledApprovals);
|
|
529
|
+
const completedToolCount = runEvents.filter((m) => m.type === "tool_call" && m.status === "start").length;
|
|
530
|
+
|
|
531
|
+
return (
|
|
532
|
+
<div key={run.id}>
|
|
533
|
+
<UserMessage>{run.prompt}</UserMessage>
|
|
534
|
+
{completedSegments.map((seg, i) => {
|
|
535
|
+
switch (seg.kind) {
|
|
536
|
+
case "thinking":
|
|
537
|
+
return <div key={i} className="mb-2"><span className="text-[11px] text-[var(--text-muted)] italic">Thought about the request</span></div>;
|
|
538
|
+
case "tools":
|
|
539
|
+
return <ToolSegmentBlock key={`tools-${i}`} tools={seg.tools} isLive={false} />;
|
|
540
|
+
case "text":
|
|
541
|
+
return (
|
|
542
|
+
<AgentResponse key={`text-${i}`}>
|
|
543
|
+
<div className="text-[13px] leading-relaxed text-[var(--text-secondary)] break-words">
|
|
544
|
+
<Markdown>{seg.text}</Markdown>
|
|
545
|
+
</div>
|
|
546
|
+
</AgentResponse>
|
|
547
|
+
);
|
|
548
|
+
case "error":
|
|
549
|
+
return (
|
|
550
|
+
<AgentResponse key={`error-${i}`}>
|
|
551
|
+
{isCreditError(seg.message) ? <CreditErrorCard /> : <p className="text-[13px] text-red-400">{seg.message}</p>}
|
|
552
|
+
</AgentResponse>
|
|
553
|
+
);
|
|
554
|
+
default:
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
})}
|
|
558
|
+
{run.status === "completed" && (
|
|
559
|
+
<RunMeta toolCount={completedToolCount} costUsd={run.cost_usd} durationMs={run.duration_ms} />
|
|
560
|
+
)}
|
|
561
|
+
</div>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Fallback: no events stored
|
|
566
|
+
const storedTools = meta?.tools_used as string[] | undefined;
|
|
567
|
+
return (
|
|
568
|
+
<div key={run.id}>
|
|
569
|
+
<UserMessage>{run.prompt}</UserMessage>
|
|
570
|
+
{storedTools && storedTools.length > 0 ? (
|
|
571
|
+
<ToolSegmentBlock tools={storedTools.map((t) => ({ name: t, done: true }))} isLive={false} />
|
|
572
|
+
) : storedToolCount != null && storedToolCount > 0 ? (
|
|
573
|
+
<RunMeta toolCount={storedToolCount} />
|
|
574
|
+
) : null}
|
|
575
|
+
<AgentResponse>
|
|
576
|
+
{run.status === "completed" && run.output ? (
|
|
577
|
+
<div className="text-[13px] leading-relaxed text-[var(--text-secondary)] break-words">
|
|
578
|
+
<Markdown>{run.output}</Markdown>
|
|
579
|
+
</div>
|
|
580
|
+
) : run.status === "failed" ? (
|
|
581
|
+
<p className="text-[13px] text-red-400">Failed{run.error ? `: ${run.error}` : ""}</p>
|
|
582
|
+
) : run.status === "stopped" ? (
|
|
583
|
+
<p className="text-[13px] text-amber-400/70 italic">Stopped</p>
|
|
584
|
+
) : (
|
|
585
|
+
<p className="text-[13px] text-[var(--text-muted)] italic">{run.status}</p>
|
|
586
|
+
)}
|
|
587
|
+
</AgentResponse>
|
|
588
|
+
{run.status === "completed" && (
|
|
589
|
+
<RunMeta toolCount={storedToolCount || 0} costUsd={run.cost_usd} durationMs={run.duration_ms} />
|
|
590
|
+
)}
|
|
591
|
+
</div>
|
|
592
|
+
);
|
|
593
|
+
})}
|
|
594
|
+
|
|
595
|
+
{/* Live: user prompt */}
|
|
596
|
+
{currentPrompt && (isRunning || messages.length > 0) && (
|
|
597
|
+
<UserMessage>{currentPrompt}</UserMessage>
|
|
598
|
+
)}
|
|
599
|
+
|
|
600
|
+
{/* Live: starting state */}
|
|
601
|
+
{(isRunning || messages.length > 0) && segments.length === 0 && isRunning && (
|
|
602
|
+
<div className="mb-3 flex items-center gap-2">
|
|
603
|
+
<span className="relative flex h-2 w-2">
|
|
604
|
+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--text-muted)] opacity-40" />
|
|
605
|
+
<span className="relative inline-flex h-2 w-2 rounded-full bg-[var(--text-muted)]" />
|
|
606
|
+
</span>
|
|
607
|
+
<span className="text-[11px] text-[var(--text-muted)]">Starting...</span>
|
|
608
|
+
</div>
|
|
609
|
+
)}
|
|
610
|
+
|
|
611
|
+
{/* Live: segments */}
|
|
612
|
+
{segments.map((seg, i) => {
|
|
613
|
+
const isLast = i === segments.length - 1;
|
|
614
|
+
|
|
615
|
+
switch (seg.kind) {
|
|
616
|
+
case "thinking":
|
|
617
|
+
return isLast && liveState === "thinking" ? (
|
|
618
|
+
<ThinkingIndicator key={i} />
|
|
619
|
+
) : (
|
|
620
|
+
<div key={i} className="mb-2"><span className="text-[11px] text-[var(--text-muted)] italic">Thought about the request</span></div>
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
case "tools":
|
|
624
|
+
return (
|
|
625
|
+
<ToolSegmentBlock
|
|
626
|
+
key={`tools-${i}`}
|
|
627
|
+
tools={seg.tools}
|
|
628
|
+
isLive={isLast && (liveState === "tool" || liveState === "idle") && isRunning && !doneMsg}
|
|
629
|
+
/>
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
case "text":
|
|
633
|
+
return (
|
|
634
|
+
<AgentResponse key={`text-${i}`} live={isLast && isRunning && !doneMsg}>
|
|
635
|
+
<div className="text-[13px] leading-relaxed text-[var(--text-secondary)] break-words">
|
|
636
|
+
<Markdown>{seg.text}</Markdown>
|
|
637
|
+
{isLast && isRunning && !doneMsg && liveState === "streaming" && (
|
|
638
|
+
<span className="inline-block w-[2px] h-[13px] bg-[var(--accent)] ml-0.5 animate-pulse align-text-bottom" />
|
|
639
|
+
)}
|
|
640
|
+
</div>
|
|
641
|
+
</AgentResponse>
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
case "approval":
|
|
645
|
+
return <ApprovalCard key={`approval-${i}`} data={seg.data} onApprove={handleApproval} />;
|
|
646
|
+
|
|
647
|
+
case "error":
|
|
648
|
+
return (
|
|
649
|
+
<AgentResponse key={`error-${i}`}>
|
|
650
|
+
{isCreditError(seg.message) ? <CreditErrorCard /> : <p className="text-[13px] text-red-400">{seg.message}</p>}
|
|
651
|
+
</AgentResponse>
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
default:
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
})}
|
|
658
|
+
|
|
659
|
+
{/* Completion meta */}
|
|
660
|
+
{doneMsg && (
|
|
661
|
+
<RunMeta
|
|
662
|
+
toolCount={toolCount}
|
|
663
|
+
costUsd={doneMsg.cost_usd as number | undefined}
|
|
664
|
+
durationMs={doneMsg.duration_ms as number | undefined}
|
|
665
|
+
/>
|
|
666
|
+
)}
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
);
|
|
670
|
+
}
|