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,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "personal-assistant-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev --port 3000",
|
|
7
|
+
"dev:worker": "tsx watch worker/index.ts",
|
|
8
|
+
"dev:all": "concurrently \"npm run dev\" \"npm run dev:worker\"",
|
|
9
|
+
"build": "next build",
|
|
10
|
+
"start": "next start",
|
|
11
|
+
"start:worker": "tsx worker/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2",
|
|
15
|
+
"@supabase/ssr": "^0.9.0",
|
|
16
|
+
"@supabase/supabase-js": "^2",
|
|
17
|
+
"bullmq": "^5",
|
|
18
|
+
"cronstrue": "^2",
|
|
19
|
+
"dotenv": "^16",
|
|
20
|
+
"gray-matter": "^4",
|
|
21
|
+
"next": "^15",
|
|
22
|
+
"react": "^19",
|
|
23
|
+
"react-dom": "^19",
|
|
24
|
+
"react-markdown": "^10.1.0",
|
|
25
|
+
"remark-gfm": "^4.0.1"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@tailwindcss/postcss": "^4",
|
|
29
|
+
"@types/node": "^22",
|
|
30
|
+
"@types/react": "^19",
|
|
31
|
+
"@types/react-dom": "^19.2.3",
|
|
32
|
+
"concurrently": "^9",
|
|
33
|
+
"postcss": "^8",
|
|
34
|
+
"tailwindcss": "^4",
|
|
35
|
+
"tsx": "^4",
|
|
36
|
+
"typescript": "^5"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export default function AgentLoading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex flex-col h-[calc(100vh-3rem)] -m-6 animate-pulse">
|
|
4
|
+
{/* Header skeleton */}
|
|
5
|
+
<div className="flex items-center gap-2.5 px-5 py-3 border-b border-[var(--border-subtle)]">
|
|
6
|
+
<div className="h-5 w-5 rounded bg-[var(--bg-elevated)]" />
|
|
7
|
+
<div className="space-y-1.5">
|
|
8
|
+
<div className="h-3.5 w-40 rounded bg-[var(--bg-elevated)]" />
|
|
9
|
+
<div className="h-3 w-64 rounded bg-[var(--bg-raised)]" />
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
{/* Chat area skeleton */}
|
|
14
|
+
<div className="flex-1 flex flex-col items-center justify-center gap-3">
|
|
15
|
+
<div className="h-3 w-32 rounded bg-[var(--bg-raised)]" />
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
{/* Message bar skeleton */}
|
|
19
|
+
<div className="border-t border-[var(--border-subtle)] px-4 py-3">
|
|
20
|
+
<div className="max-w-2xl mx-auto">
|
|
21
|
+
<div className="h-10 rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-raised)]" />
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback, useRef, use } from "react";
|
|
4
|
+
import { useSearchParams, useRouter } from "next/navigation";
|
|
5
|
+
import { ChatView } from "@/components/chat-view";
|
|
6
|
+
import { MessageBar } from "@/components/message-bar";
|
|
7
|
+
import { useSSE, type SSEMessage } from "@/hooks/use-sse";
|
|
8
|
+
import { getCachedAgent } from "@/lib/agent-cache";
|
|
9
|
+
import type { AgentMeta, AgentRun } from "@/lib/types";
|
|
10
|
+
|
|
11
|
+
export default function AgentDetailPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
12
|
+
const { slug } = use(params);
|
|
13
|
+
const searchParams = useSearchParams();
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
|
|
16
|
+
const cachedAgent = getCachedAgent(slug);
|
|
17
|
+
const [agent, setAgent] = useState<AgentMeta | null>(cachedAgent || null);
|
|
18
|
+
const [allRuns, setAllRuns] = useState<AgentRun[]>([]);
|
|
19
|
+
const [isRunning, setIsRunning] = useState(false);
|
|
20
|
+
const { messages, isConnected, connect, disconnect, reset, loadHistory } = useSSE();
|
|
21
|
+
|
|
22
|
+
// Conversation state
|
|
23
|
+
const [sessionId, setSessionId] = useState<string | null>(null);
|
|
24
|
+
const [conversationRuns, setConversationRuns] = useState<AgentRun[]>([]);
|
|
25
|
+
const [currentPrompt, setCurrentPrompt] = useState<string | null>(null);
|
|
26
|
+
const [completedRunEvents, setCompletedRunEvents] = useState<Record<string, SSEMessage[]>>({});
|
|
27
|
+
|
|
28
|
+
// Prompt suggestions for empty chat
|
|
29
|
+
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
30
|
+
|
|
31
|
+
// Track current run ID for approval POSTs
|
|
32
|
+
const currentRunIdRef = useRef<string | null>(null);
|
|
33
|
+
|
|
34
|
+
// Track whether title has been generated for this session
|
|
35
|
+
const titleGeneratedRef = useRef<Set<string>>(new Set());
|
|
36
|
+
|
|
37
|
+
// Guard: skip session-param effect when URL was just updated by handleSend
|
|
38
|
+
const justSentRef = useRef<string | null>(null);
|
|
39
|
+
|
|
40
|
+
// Track which session we're currently viewing to avoid redundant loads
|
|
41
|
+
const activeSessionRef = useRef<string | null>(null);
|
|
42
|
+
|
|
43
|
+
// True while loadSession is fetching data
|
|
44
|
+
const [sessionLoading, setSessionLoading] = useState(false);
|
|
45
|
+
|
|
46
|
+
// Track if initial session load has been triggered (prevents double-fire)
|
|
47
|
+
const initialLoadRef = useRef(false);
|
|
48
|
+
|
|
49
|
+
// Mark notifications as read for the currently viewed session
|
|
50
|
+
const markSessionNotificationsRead = useCallback((sid: string) => {
|
|
51
|
+
fetch("/api/notifications", {
|
|
52
|
+
method: "PATCH",
|
|
53
|
+
headers: { "Content-Type": "application/json" },
|
|
54
|
+
body: JSON.stringify({ session_id: sid }),
|
|
55
|
+
}).catch(() => {});
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
// --- Data loading ---
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!agent) {
|
|
62
|
+
fetch("/api/agents")
|
|
63
|
+
.then((r) => r.json())
|
|
64
|
+
.then((agents: AgentMeta[]) => {
|
|
65
|
+
setAgent(agents.find((a) => a.slug === slug) || null);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Background fetch for sidebar context — does NOT block session loading
|
|
70
|
+
fetch(`/api/runs?agent_slug=${slug}&limit=50`)
|
|
71
|
+
.then((r) => r.json())
|
|
72
|
+
.then((runs) => {
|
|
73
|
+
setAllRuns(runs);
|
|
74
|
+
});
|
|
75
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
76
|
+
}, [slug]);
|
|
77
|
+
|
|
78
|
+
// --- Fetch prompt suggestions when agent is known ---
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!agent) return;
|
|
82
|
+
|
|
83
|
+
// Static fallbacks so empty state always has suggestions, even if the API is down
|
|
84
|
+
const fallbacks = [
|
|
85
|
+
`What can you help me with?`,
|
|
86
|
+
`Summarize your capabilities`,
|
|
87
|
+
`What should I know before we start?`,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const fetchSuggestions = () => {
|
|
91
|
+
const p = new URLSearchParams({
|
|
92
|
+
slug: agent.slug,
|
|
93
|
+
name: agent.name,
|
|
94
|
+
description: agent.description,
|
|
95
|
+
});
|
|
96
|
+
fetch(`/api/ai/suggestions?${p}`)
|
|
97
|
+
.then((r) => r.json())
|
|
98
|
+
.then((d) => {
|
|
99
|
+
if (d.suggestions) setSuggestions(d.suggestions);
|
|
100
|
+
})
|
|
101
|
+
.catch(() => {});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Set fallbacks immediately, then try to fetch better ones
|
|
105
|
+
setSuggestions(fallbacks);
|
|
106
|
+
fetchSuggestions();
|
|
107
|
+
|
|
108
|
+
// Retry once after 10s in case the API was temporarily unavailable (e.g. credit propagation delay)
|
|
109
|
+
const retryTimer = setTimeout(() => {
|
|
110
|
+
// Only retry if we still have fallbacks (AI suggestions didn't arrive)
|
|
111
|
+
setSuggestions((prev) => {
|
|
112
|
+
if (prev === fallbacks || prev.length === 0) {
|
|
113
|
+
fetchSuggestions();
|
|
114
|
+
}
|
|
115
|
+
return prev;
|
|
116
|
+
});
|
|
117
|
+
}, 10_000);
|
|
118
|
+
|
|
119
|
+
return () => clearTimeout(retryTimer);
|
|
120
|
+
}, [agent?.slug, agent?.name, agent?.description]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// loadSession — the ONE function that handles all session navigation.
|
|
124
|
+
// Renders progressively: shows conversation structure first, then loads events.
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
const loadSession = useCallback(
|
|
128
|
+
async (newSessionId: string | null) => {
|
|
129
|
+
// 1. Immediately clear previous state and show loading
|
|
130
|
+
disconnect();
|
|
131
|
+
reset();
|
|
132
|
+
setIsRunning(false);
|
|
133
|
+
setCurrentPrompt(null);
|
|
134
|
+
setConversationRuns([]);
|
|
135
|
+
setCompletedRunEvents({});
|
|
136
|
+
currentRunIdRef.current = null;
|
|
137
|
+
activeSessionRef.current = newSessionId;
|
|
138
|
+
|
|
139
|
+
if (!newSessionId) {
|
|
140
|
+
setSessionId(null);
|
|
141
|
+
setSessionLoading(false);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
setSessionId(newSessionId);
|
|
146
|
+
setSessionLoading(true);
|
|
147
|
+
|
|
148
|
+
// Dismiss any notifications for this session since user is viewing it
|
|
149
|
+
markSessionNotificationsRead(newSessionId);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// 2. Fetch runs — single fetch, no duplication
|
|
153
|
+
let freshRuns: AgentRun[];
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch(`/api/runs?agent_slug=${slug}&limit=50`);
|
|
156
|
+
freshRuns = await res.json();
|
|
157
|
+
setAllRuns(freshRuns);
|
|
158
|
+
} catch {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (activeSessionRef.current !== newSessionId) return;
|
|
163
|
+
|
|
164
|
+
const sessionRuns = freshRuns
|
|
165
|
+
.filter((r) => r.session_id === newSessionId)
|
|
166
|
+
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
167
|
+
|
|
168
|
+
if (sessionRuns.length === 0) {
|
|
169
|
+
setConversationRuns([]);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const latestRun = sessionRuns[sessionRuns.length - 1];
|
|
174
|
+
|
|
175
|
+
// 3. IMMEDIATELY show conversation structure (prompts + outputs visible)
|
|
176
|
+
// Events load in background to fill in tool/thinking detail.
|
|
177
|
+
if (
|
|
178
|
+
latestRun.status === "completed" ||
|
|
179
|
+
latestRun.status === "failed" ||
|
|
180
|
+
latestRun.status === "stopped"
|
|
181
|
+
) {
|
|
182
|
+
setConversationRuns(sessionRuns);
|
|
183
|
+
setSessionLoading(false); // ← Render NOW, don't wait for events
|
|
184
|
+
|
|
185
|
+
// 4. Load events in background (enriches the display with tool details)
|
|
186
|
+
const completedRuns = sessionRuns.filter(
|
|
187
|
+
(r) => r.status === "completed" || r.status === "failed" || r.status === "stopped"
|
|
188
|
+
);
|
|
189
|
+
if (completedRuns.length > 0) {
|
|
190
|
+
const eventsMap: Record<string, SSEMessage[]> = {};
|
|
191
|
+
await Promise.all(
|
|
192
|
+
completedRuns.map(async (run) => {
|
|
193
|
+
try {
|
|
194
|
+
const res = await fetch(`/api/runs/${run.id}/events`);
|
|
195
|
+
if (!res.ok) return;
|
|
196
|
+
const rawEvents = (await res.json()) as Record<string, unknown>[];
|
|
197
|
+
if (rawEvents.length > 0) {
|
|
198
|
+
eventsMap[run.id] = rawEvents.map((evt) => ({
|
|
199
|
+
type: evt.event_type as string,
|
|
200
|
+
seq: evt.seq as number,
|
|
201
|
+
...(evt.payload as Record<string, unknown>),
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
} catch { /* skip */ }
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
if (activeSessionRef.current !== newSessionId) return;
|
|
208
|
+
setCompletedRunEvents(eventsMap);
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 5. Latest run is running/queued → show prior turns, reconnect to live
|
|
214
|
+
const priorRuns = sessionRuns.slice(0, -1);
|
|
215
|
+
setConversationRuns(priorRuns);
|
|
216
|
+
setCurrentPrompt(latestRun.prompt);
|
|
217
|
+
setIsRunning(true);
|
|
218
|
+
currentRunIdRef.current = latestRun.id;
|
|
219
|
+
setSessionLoading(false); // ← Render prior turns NOW
|
|
220
|
+
|
|
221
|
+
// Load historical events from DB
|
|
222
|
+
const { maxSeq, events } = await loadHistory(latestRun.id);
|
|
223
|
+
|
|
224
|
+
if (activeSessionRef.current !== newSessionId) return;
|
|
225
|
+
|
|
226
|
+
const alreadyDone = events.some(
|
|
227
|
+
(e) => e.type === "done" || e.type === "stopped" || (e.type === "error" && !events.some((d) => d.type === "done" || d.type === "stopped"))
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (alreadyDone) {
|
|
231
|
+
setIsRunning(false);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
connect(`/api/runs/${latestRun.id}/stream`, maxSeq > 0 ? maxSeq : undefined);
|
|
236
|
+
|
|
237
|
+
// Load events for prior completed runs in background
|
|
238
|
+
if (priorRuns.length > 0) {
|
|
239
|
+
const eventsMap: Record<string, SSEMessage[]> = {};
|
|
240
|
+
await Promise.all(
|
|
241
|
+
priorRuns.map(async (run) => {
|
|
242
|
+
try {
|
|
243
|
+
const res = await fetch(`/api/runs/${run.id}/events`);
|
|
244
|
+
if (!res.ok) return;
|
|
245
|
+
const rawEvents = (await res.json()) as Record<string, unknown>[];
|
|
246
|
+
if (rawEvents.length > 0) {
|
|
247
|
+
eventsMap[run.id] = rawEvents.map((evt) => ({
|
|
248
|
+
type: evt.event_type as string,
|
|
249
|
+
seq: evt.seq as number,
|
|
250
|
+
...(evt.payload as Record<string, unknown>),
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
} catch { /* skip */ }
|
|
254
|
+
})
|
|
255
|
+
);
|
|
256
|
+
if (activeSessionRef.current !== newSessionId) return;
|
|
257
|
+
setCompletedRunEvents(eventsMap);
|
|
258
|
+
}
|
|
259
|
+
} finally {
|
|
260
|
+
setSessionLoading(false);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
[slug, disconnect, reset, loadHistory, connect, markSessionNotificationsRead]
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// Session navigation: when ?session=X changes, load immediately.
|
|
268
|
+
// No longer waits for runsLoaded — loadSession fetches its own data.
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
const sessionParam = searchParams.get("session");
|
|
273
|
+
|
|
274
|
+
// Guard: skip if handleSend just set this URL
|
|
275
|
+
if (sessionParam && justSentRef.current === sessionParam) {
|
|
276
|
+
justSentRef.current = null;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Don't reload if already viewing this session
|
|
281
|
+
if (sessionParam === sessionId) return;
|
|
282
|
+
|
|
283
|
+
// Prevent double-fire on initial mount
|
|
284
|
+
if (!initialLoadRef.current) {
|
|
285
|
+
initialLoadRef.current = true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
loadSession(sessionParam || null);
|
|
289
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
290
|
+
}, [searchParams]);
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Stream completion: when the live stream finishes, update sidebar + metadata
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
const doneMsg = messages.find(
|
|
298
|
+
(m) => m.type === "done" || m.type === "stopped"
|
|
299
|
+
);
|
|
300
|
+
const statusMsg = messages.find(
|
|
301
|
+
(m) =>
|
|
302
|
+
m.type === "status" &&
|
|
303
|
+
(m.status === "completed" || m.status === "failed" || m.status === "stopped")
|
|
304
|
+
);
|
|
305
|
+
const errorMsg = messages.find(
|
|
306
|
+
(m) =>
|
|
307
|
+
m.type === "error" &&
|
|
308
|
+
!messages.some((d) => d.type === "done" || d.type === "stopped")
|
|
309
|
+
);
|
|
310
|
+
const finished = doneMsg || statusMsg || errorMsg;
|
|
311
|
+
|
|
312
|
+
if (finished && !isConnected) {
|
|
313
|
+
setIsRunning(false);
|
|
314
|
+
|
|
315
|
+
if (sessionId) {
|
|
316
|
+
// User is watching this chat live — dismiss any notifications for it
|
|
317
|
+
markSessionNotificationsRead(sessionId);
|
|
318
|
+
|
|
319
|
+
fetch(`/api/runs?agent_slug=${slug}&limit=50`)
|
|
320
|
+
.then((r) => r.json())
|
|
321
|
+
.then((fetchedRuns: AgentRun[]) => {
|
|
322
|
+
setAllRuns(fetchedRuns);
|
|
323
|
+
window.dispatchEvent(new Event("agent-session-updated"));
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (!titleGeneratedRef.current.has(sessionId) && currentPrompt) {
|
|
327
|
+
titleGeneratedRef.current.add(sessionId);
|
|
328
|
+
fetch("/api/ai/title", {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: { "Content-Type": "application/json" },
|
|
331
|
+
body: JSON.stringify({ session_id: sessionId, message: currentPrompt }),
|
|
332
|
+
})
|
|
333
|
+
.then((r) => r.json())
|
|
334
|
+
.then((d) => {
|
|
335
|
+
if (d.title) {
|
|
336
|
+
window.dispatchEvent(
|
|
337
|
+
new CustomEvent("session-title-ready", {
|
|
338
|
+
detail: { session_id: sessionId, title: d.title },
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
.catch((err) => console.error("[title] generation failed:", err));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Save tool details to run metadata
|
|
348
|
+
const runId = currentRunIdRef.current;
|
|
349
|
+
const toolCalls = messages.filter(
|
|
350
|
+
(m) => m.type === "tool_call" && m.status === "start"
|
|
351
|
+
);
|
|
352
|
+
if (runId && toolCalls.length > 0) {
|
|
353
|
+
const toolNames = toolCalls.map((m) => m.name as string);
|
|
354
|
+
fetch(`/api/runs/${runId}/metadata`, {
|
|
355
|
+
method: "PATCH",
|
|
356
|
+
headers: { "Content-Type": "application/json" },
|
|
357
|
+
body: JSON.stringify({
|
|
358
|
+
tool_count: toolCalls.length,
|
|
359
|
+
tools_used: toolNames,
|
|
360
|
+
}),
|
|
361
|
+
}).catch(() => {});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
365
|
+
}, [messages, isConnected]);
|
|
366
|
+
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
// Send message
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
const handleSend = useCallback(
|
|
372
|
+
async (prompt: string, files?: File[]) => {
|
|
373
|
+
if (!agent) return;
|
|
374
|
+
|
|
375
|
+
// Before clearing live state, snapshot previous turns into conversationRuns
|
|
376
|
+
if (sessionId && allRuns.length > 0) {
|
|
377
|
+
const convRuns = allRuns
|
|
378
|
+
.filter((r) => r.session_id === sessionId)
|
|
379
|
+
.sort(
|
|
380
|
+
(a, b) =>
|
|
381
|
+
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
|
382
|
+
);
|
|
383
|
+
setConversationRuns(convRuns);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Generate session ID for new conversations so sidebar shows immediately
|
|
387
|
+
const isNewSession = !sessionId;
|
|
388
|
+
const sid = sessionId || crypto.randomUUID();
|
|
389
|
+
if (isNewSession) {
|
|
390
|
+
justSentRef.current = sid;
|
|
391
|
+
setSessionId(sid);
|
|
392
|
+
activeSessionRef.current = sid;
|
|
393
|
+
router.replace(`/agents/${slug}/chat?session=${sid}`, { scroll: false });
|
|
394
|
+
window.dispatchEvent(
|
|
395
|
+
new CustomEvent("agent-session-started", {
|
|
396
|
+
detail: { session_id: sid },
|
|
397
|
+
})
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// Fire-and-forget title generation immediately
|
|
401
|
+
titleGeneratedRef.current.add(sid);
|
|
402
|
+
fetch("/api/ai/title", {
|
|
403
|
+
method: "POST",
|
|
404
|
+
headers: { "Content-Type": "application/json" },
|
|
405
|
+
body: JSON.stringify({ session_id: sid, message: prompt }),
|
|
406
|
+
})
|
|
407
|
+
.then((r) => r.json())
|
|
408
|
+
.then((d) => {
|
|
409
|
+
if (d.title) {
|
|
410
|
+
window.dispatchEvent(
|
|
411
|
+
new CustomEvent("session-title-ready", {
|
|
412
|
+
detail: { session_id: sid, title: d.title },
|
|
413
|
+
})
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
.catch((err) => console.error("[title] generation failed:", err));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
disconnect();
|
|
421
|
+
reset();
|
|
422
|
+
setIsRunning(true);
|
|
423
|
+
setCurrentPrompt(prompt);
|
|
424
|
+
|
|
425
|
+
// 1. Create run record
|
|
426
|
+
const res = await fetch("/api/runs", {
|
|
427
|
+
method: "POST",
|
|
428
|
+
headers: { "Content-Type": "application/json" },
|
|
429
|
+
body: JSON.stringify({
|
|
430
|
+
agent_slug: agent.slug,
|
|
431
|
+
agent_name: agent.name,
|
|
432
|
+
prompt,
|
|
433
|
+
session_id: sid,
|
|
434
|
+
}),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
if (!res.ok) {
|
|
438
|
+
setIsRunning(false);
|
|
439
|
+
setCurrentPrompt(null);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const run = await res.json();
|
|
444
|
+
currentRunIdRef.current = run.id;
|
|
445
|
+
|
|
446
|
+
// 2. Upload attached files if any
|
|
447
|
+
let filePaths: string[] = [];
|
|
448
|
+
if (files && files.length > 0) {
|
|
449
|
+
const form = new FormData();
|
|
450
|
+
form.append("run_id", run.id);
|
|
451
|
+
for (const f of files) form.append("files", f);
|
|
452
|
+
try {
|
|
453
|
+
const uploadRes = await fetch("/api/uploads", {
|
|
454
|
+
method: "POST",
|
|
455
|
+
body: form,
|
|
456
|
+
});
|
|
457
|
+
if (uploadRes.ok) {
|
|
458
|
+
const { files: saved } = await uploadRes.json();
|
|
459
|
+
filePaths = saved.map((f: { path: string }) => f.path);
|
|
460
|
+
}
|
|
461
|
+
} catch {
|
|
462
|
+
// Continue without files if upload fails
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// 3. Start detached execution
|
|
467
|
+
await fetch(`/api/runs/${run.id}/start`, {
|
|
468
|
+
method: "POST",
|
|
469
|
+
headers: { "Content-Type": "application/json" },
|
|
470
|
+
body: JSON.stringify(filePaths.length > 0 ? { files: filePaths } : {}),
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// 4. Observe via SSE
|
|
474
|
+
connect(`/api/runs/${run.id}/stream`);
|
|
475
|
+
},
|
|
476
|
+
[agent, sessionId, allRuns, slug, connect, disconnect, reset, router]
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
const handleStop = useCallback(async () => {
|
|
480
|
+
const runId = currentRunIdRef.current;
|
|
481
|
+
if (runId) {
|
|
482
|
+
fetch(`/api/runs/${runId}/stop`, { method: "POST" }).catch(() => {});
|
|
483
|
+
}
|
|
484
|
+
disconnect();
|
|
485
|
+
setIsRunning(false);
|
|
486
|
+
}, [disconnect]);
|
|
487
|
+
|
|
488
|
+
const handleApprove = useCallback(
|
|
489
|
+
async (toolUseId: string, approved: boolean) => {
|
|
490
|
+
const runId = currentRunIdRef.current;
|
|
491
|
+
if (!runId) return;
|
|
492
|
+
|
|
493
|
+
await fetch(`/api/runs/${runId}/approve`, {
|
|
494
|
+
method: "POST",
|
|
495
|
+
headers: { "Content-Type": "application/json" },
|
|
496
|
+
body: JSON.stringify({ tool_use_id: toolUseId, approved }),
|
|
497
|
+
});
|
|
498
|
+
},
|
|
499
|
+
[]
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
const agentReady = !!agent;
|
|
503
|
+
const inConversation =
|
|
504
|
+
sessionId !== null ||
|
|
505
|
+
conversationRuns.length > 0 ||
|
|
506
|
+
isRunning ||
|
|
507
|
+
(currentPrompt !== null && messages.length > 0);
|
|
508
|
+
|
|
509
|
+
return (
|
|
510
|
+
<div className="flex flex-col h-[calc(100vh-3rem)] -m-6">
|
|
511
|
+
{/* Header */}
|
|
512
|
+
<div className="flex items-center justify-between px-5 py-3 border-b border-[var(--border-subtle)]">
|
|
513
|
+
<div className="flex items-center gap-2.5 min-w-0">
|
|
514
|
+
{!agentReady ? (
|
|
515
|
+
<div className="animate-pulse flex items-center gap-2.5">
|
|
516
|
+
<div className="h-5 w-5 rounded bg-[var(--bg-elevated)]" />
|
|
517
|
+
<div className="space-y-1.5">
|
|
518
|
+
<div className="h-3.5 w-40 rounded bg-[var(--bg-elevated)]" />
|
|
519
|
+
<div className="h-3 w-64 rounded bg-[var(--bg-raised)]" />
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
) : (
|
|
523
|
+
<>
|
|
524
|
+
{agent.emoji && <span className="text-lg">{agent.emoji}</span>}
|
|
525
|
+
<div className="min-w-0">
|
|
526
|
+
<h1 className="text-[13px] font-semibold text-[var(--text-primary)] truncate">
|
|
527
|
+
{agent.name}
|
|
528
|
+
</h1>
|
|
529
|
+
<p className="text-[11px] text-[var(--text-muted)] truncate">
|
|
530
|
+
{agent.description}
|
|
531
|
+
</p>
|
|
532
|
+
</div>
|
|
533
|
+
</>
|
|
534
|
+
)}
|
|
535
|
+
</div>
|
|
536
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
537
|
+
{isConnected && (
|
|
538
|
+
<span className="flex items-center gap-1.5 text-[11px] text-green-500/70">
|
|
539
|
+
<span className="h-1.5 w-1.5 rounded-full bg-green-500/70 animate-pulse" />
|
|
540
|
+
Live
|
|
541
|
+
</span>
|
|
542
|
+
)}
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
|
|
546
|
+
{/* Chat area */}
|
|
547
|
+
<ChatView
|
|
548
|
+
conversationRuns={conversationRuns}
|
|
549
|
+
completedRunEvents={completedRunEvents}
|
|
550
|
+
messages={messages}
|
|
551
|
+
isConnected={isConnected}
|
|
552
|
+
isRunning={isRunning}
|
|
553
|
+
currentPrompt={currentPrompt}
|
|
554
|
+
onApprove={handleApprove}
|
|
555
|
+
loading={sessionLoading}
|
|
556
|
+
suggestions={suggestions}
|
|
557
|
+
onSuggestionClick={(text) => handleSend(text)}
|
|
558
|
+
agentName={agent?.name}
|
|
559
|
+
agentEmoji={agent?.emoji}
|
|
560
|
+
/>
|
|
561
|
+
|
|
562
|
+
{/* Message bar */}
|
|
563
|
+
<MessageBar
|
|
564
|
+
key={sessionId || "new"}
|
|
565
|
+
onSend={handleSend}
|
|
566
|
+
onStop={handleStop}
|
|
567
|
+
disabled={isRunning || !agentReady}
|
|
568
|
+
isRunning={isRunning && isConnected}
|
|
569
|
+
placeholder={
|
|
570
|
+
!agentReady
|
|
571
|
+
? "Loading..."
|
|
572
|
+
: inConversation
|
|
573
|
+
? "Follow up..."
|
|
574
|
+
: `Message ${agent.name}...`
|
|
575
|
+
}
|
|
576
|
+
/>
|
|
577
|
+
</div>
|
|
578
|
+
);
|
|
579
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export default function AgentLoading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="max-w-4xl mx-auto py-8 px-2 animate-pulse">
|
|
4
|
+
<div className="h-3 w-16 rounded bg-[var(--bg-raised)] mb-6" />
|
|
5
|
+
<div className="flex items-start gap-4 mb-8">
|
|
6
|
+
<div className="h-8 w-8 rounded-md bg-[var(--bg-elevated)]" />
|
|
7
|
+
<div className="flex-1">
|
|
8
|
+
<div className="h-5 w-48 rounded bg-[var(--bg-elevated)] mb-2" />
|
|
9
|
+
<div className="h-3.5 w-96 rounded bg-[var(--bg-raised)]" />
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
<div className="space-y-2">
|
|
13
|
+
{[...Array(4)].map((_, i) => (
|
|
14
|
+
<div key={i} className="h-10 rounded-md bg-[var(--bg-raised)]" />
|
|
15
|
+
))}
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getAgents } from "@/lib/agents";
|
|
2
|
+
import { AgentGrid } from "@/components/agent-grid";
|
|
3
|
+
|
|
4
|
+
export default function AgentsPage() {
|
|
5
|
+
const agents = getAgents();
|
|
6
|
+
const specialists = agents.filter((a) => a.slug !== "main");
|
|
7
|
+
return <AgentGrid agents={specialists} />;
|
|
8
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getAgentCapabilities } from "@/lib/capabilities";
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
_req: Request,
|
|
6
|
+
{ params }: { params: Promise<{ slug: string }> }
|
|
7
|
+
) {
|
|
8
|
+
const { slug } = await params;
|
|
9
|
+
const capabilities = getAgentCapabilities(slug);
|
|
10
|
+
return NextResponse.json(capabilities);
|
|
11
|
+
}
|