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,226 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import type { AgentRun } from "@/lib/types";
|
|
6
|
+
import { ChevronRightIcon, DollarIcon, ClockIcon, MessageIcon, AlertIcon, UserIcon, ScheduleIcon } from "@/components/icons";
|
|
7
|
+
|
|
8
|
+
const statusConfig: Record<
|
|
9
|
+
string,
|
|
10
|
+
{ dot: string; label: string }
|
|
11
|
+
> = {
|
|
12
|
+
queued: { dot: "bg-yellow-400", label: "Queued" },
|
|
13
|
+
running: { dot: "bg-[var(--accent)] animate-pulse", label: "Running" },
|
|
14
|
+
completed: { dot: "bg-emerald-400", label: "Completed" },
|
|
15
|
+
failed: { dot: "bg-red-400", label: "Failed" },
|
|
16
|
+
stopped: { dot: "bg-[var(--text-muted)]", label: "Stopped" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type StatusFilter = "all" | "running" | "completed" | "failed" | "queued" | "stopped";
|
|
20
|
+
type SourceFilter = "all" | "manual" | "scheduled";
|
|
21
|
+
|
|
22
|
+
function timeAgo(date: string): string {
|
|
23
|
+
const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000);
|
|
24
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
25
|
+
const minutes = Math.floor(seconds / 60);
|
|
26
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
27
|
+
const hours = Math.floor(minutes / 60);
|
|
28
|
+
if (hours < 24) return `${hours}h ago`;
|
|
29
|
+
const days = Math.floor(hours / 24);
|
|
30
|
+
return `${days}d ago`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatDuration(ms: number): string {
|
|
34
|
+
if (ms < 1000) return `${ms}ms`;
|
|
35
|
+
const s = ms / 1000;
|
|
36
|
+
if (s < 60) return `${s.toFixed(1)}s`;
|
|
37
|
+
const m = Math.floor(s / 60);
|
|
38
|
+
const remaining = Math.floor(s % 60);
|
|
39
|
+
return `${m}m ${remaining}s`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runHref(run: AgentRun): string {
|
|
43
|
+
const base = `/agents/${run.agent_slug}/chat`;
|
|
44
|
+
return run.session_id ? `${base}?session=${run.session_id}` : base;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function RunRow({ run }: { run: AgentRun }) {
|
|
48
|
+
const status = statusConfig[run.status] || statusConfig.stopped;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Link
|
|
52
|
+
href={runHref(run)}
|
|
53
|
+
className="group flex items-center gap-3 px-3 py-2.5 transition-colors hover:bg-[var(--bg-hover)]"
|
|
54
|
+
>
|
|
55
|
+
{/* Status dot */}
|
|
56
|
+
<span className={`h-2 w-2 rounded-full shrink-0 ${status.dot}`} />
|
|
57
|
+
|
|
58
|
+
{/* Agent name */}
|
|
59
|
+
<span className="text-[13px] font-medium text-[var(--text-primary)] w-32 shrink-0 truncate">
|
|
60
|
+
{run.agent_name}
|
|
61
|
+
</span>
|
|
62
|
+
|
|
63
|
+
{/* Prompt */}
|
|
64
|
+
<span className="text-[12px] text-[var(--text-tertiary)] truncate flex-1 min-w-0">
|
|
65
|
+
{run.prompt || "—"}
|
|
66
|
+
</span>
|
|
67
|
+
|
|
68
|
+
{/* Meta: cost, duration, events */}
|
|
69
|
+
<div className="flex items-center gap-3 shrink-0 text-[11px] text-[var(--text-muted)] tabular-nums">
|
|
70
|
+
{run.cost_usd != null && (
|
|
71
|
+
<span className="flex items-center gap-1">
|
|
72
|
+
<DollarIcon size={12} className="opacity-50" />
|
|
73
|
+
${run.cost_usd.toFixed(4)}
|
|
74
|
+
</span>
|
|
75
|
+
)}
|
|
76
|
+
{run.duration_ms != null && (
|
|
77
|
+
<span className="flex items-center gap-1">
|
|
78
|
+
<ClockIcon size={12} className="opacity-50" />
|
|
79
|
+
{formatDuration(run.duration_ms)}
|
|
80
|
+
</span>
|
|
81
|
+
)}
|
|
82
|
+
{run.event_count > 0 && (
|
|
83
|
+
<span className="flex items-center gap-1">
|
|
84
|
+
<MessageIcon size={12} className="opacity-50" />
|
|
85
|
+
{run.event_count}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
{run.error && (
|
|
89
|
+
<AlertIcon size={12} className="text-red-400/60" />
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Time */}
|
|
94
|
+
<span className="text-[11px] text-[var(--text-muted)] w-16 text-right shrink-0 tabular-nums">
|
|
95
|
+
{timeAgo(run.created_at)}
|
|
96
|
+
</span>
|
|
97
|
+
|
|
98
|
+
{/* Chevron */}
|
|
99
|
+
<ChevronRightIcon
|
|
100
|
+
size={14}
|
|
101
|
+
className="text-[var(--text-muted)] opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
|
|
102
|
+
/>
|
|
103
|
+
</Link>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function RunHistoryTable({ runs }: { runs: AgentRun[] }) {
|
|
108
|
+
const [filter, setFilter] = useState<StatusFilter>("all");
|
|
109
|
+
const [source, setSource] = useState<SourceFilter>("all");
|
|
110
|
+
|
|
111
|
+
const manualCount = runs.filter((r) => !r.schedule_id).length;
|
|
112
|
+
const scheduledCount = runs.filter((r) => !!r.schedule_id).length;
|
|
113
|
+
|
|
114
|
+
const sourceFiltered =
|
|
115
|
+
source === "all" ? runs : source === "manual" ? runs.filter((r) => !r.schedule_id) : runs.filter((r) => !!r.schedule_id);
|
|
116
|
+
|
|
117
|
+
const counts = sourceFiltered.reduce(
|
|
118
|
+
(acc, r) => {
|
|
119
|
+
acc[r.status] = (acc[r.status] || 0) + 1;
|
|
120
|
+
return acc;
|
|
121
|
+
},
|
|
122
|
+
{} as Record<string, number>,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const filtered = filter === "all" ? sourceFiltered : sourceFiltered.filter((r) => r.status === filter);
|
|
126
|
+
|
|
127
|
+
const activeFilters = (["all", "running", "completed", "failed", "queued", "stopped"] as const).filter(
|
|
128
|
+
(f) => f === "all" || counts[f],
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (runs.length === 0) {
|
|
132
|
+
return (
|
|
133
|
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
134
|
+
<p className="text-[13px] text-[var(--text-tertiary)]">No runs yet</p>
|
|
135
|
+
<p className="mt-1 text-[12px] text-[var(--text-muted)]">
|
|
136
|
+
Run an agent to see its history here
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className="space-y-3">
|
|
144
|
+
{/* Source toggle + Status filter pills */}
|
|
145
|
+
<div className="flex items-center gap-4">
|
|
146
|
+
{/* Source toggle */}
|
|
147
|
+
<div className="flex items-center rounded-lg border border-[var(--border-subtle)] p-0.5">
|
|
148
|
+
{(
|
|
149
|
+
[
|
|
150
|
+
{ key: "all", label: "All", icon: null, count: runs.length },
|
|
151
|
+
{ key: "manual", label: "Manual", icon: UserIcon, count: manualCount },
|
|
152
|
+
{ key: "scheduled", label: "Scheduled", icon: ScheduleIcon, count: scheduledCount },
|
|
153
|
+
] as const
|
|
154
|
+
).map(({ key, label, icon: IconCmp, count }) => {
|
|
155
|
+
const isActive = source === key;
|
|
156
|
+
return (
|
|
157
|
+
<button
|
|
158
|
+
key={key}
|
|
159
|
+
onClick={() => { setSource(key); setFilter("all"); }}
|
|
160
|
+
className={`inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-[11px] font-medium transition-colors ${
|
|
161
|
+
isActive
|
|
162
|
+
? "bg-[var(--bg-active)] text-[var(--text-primary)]"
|
|
163
|
+
: "text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]"
|
|
164
|
+
}`}
|
|
165
|
+
>
|
|
166
|
+
{IconCmp && <IconCmp size={12} className="opacity-60" />}
|
|
167
|
+
{label}
|
|
168
|
+
<span className="opacity-50 tabular-nums">{count}</span>
|
|
169
|
+
</button>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Status filter pills */}
|
|
175
|
+
{activeFilters.length > 2 && (
|
|
176
|
+
<div className="flex items-center gap-1 border-l border-[var(--border-subtle)] pl-4">
|
|
177
|
+
{activeFilters.map((f) => {
|
|
178
|
+
const isActive = filter === f;
|
|
179
|
+
const count = f === "all" ? sourceFiltered.length : counts[f] || 0;
|
|
180
|
+
const config = f !== "all" ? statusConfig[f] : null;
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<button
|
|
184
|
+
key={f}
|
|
185
|
+
onClick={() => setFilter(f)}
|
|
186
|
+
className={`inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-medium transition-colors ${
|
|
187
|
+
isActive
|
|
188
|
+
? "bg-[var(--bg-active)] text-[var(--text-primary)]"
|
|
189
|
+
: "text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]"
|
|
190
|
+
}`}
|
|
191
|
+
>
|
|
192
|
+
{config && <span className={`h-1.5 w-1.5 rounded-full ${config.dot}`} />}
|
|
193
|
+
{f === "all" ? "All" : config?.label || f}
|
|
194
|
+
<span className="opacity-50 tabular-nums">{count}</span>
|
|
195
|
+
</button>
|
|
196
|
+
);
|
|
197
|
+
})}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{/* Table header */}
|
|
203
|
+
<div className="flex items-center gap-3 px-3 py-1.5 text-[10px] font-medium uppercase tracking-wider text-[var(--text-muted)]">
|
|
204
|
+
<span className="w-2 shrink-0" />
|
|
205
|
+
<span className="w-32 shrink-0">Agent</span>
|
|
206
|
+
<span className="flex-1">Prompt</span>
|
|
207
|
+
<span className="w-[200px] shrink-0 text-right">Details</span>
|
|
208
|
+
<span className="w-16 text-right shrink-0">When</span>
|
|
209
|
+
<span className="w-[14px] shrink-0" />
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{/* Run rows */}
|
|
213
|
+
<div className="border border-[var(--border-subtle)] rounded-lg divide-y divide-[var(--border-subtle)]">
|
|
214
|
+
{filtered.map((run) => (
|
|
215
|
+
<RunRow key={run.id} run={run} />
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{filtered.length === 0 && (filter !== "all" || source !== "all") && (
|
|
220
|
+
<p className="text-[12px] text-[var(--text-muted)] text-center py-8">
|
|
221
|
+
No {filter !== "all" ? (statusConfig[filter]?.label.toLowerCase() + " ") : ""}{source !== "all" ? source + " " : ""}runs
|
|
222
|
+
</p>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import type { SSEMessage } from "@/hooks/use-sse";
|
|
5
|
+
|
|
6
|
+
interface ApprovalRequest {
|
|
7
|
+
tool_name: string;
|
|
8
|
+
tool_input: Record<string, unknown>;
|
|
9
|
+
tool_use_id: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface RunOutputProps {
|
|
13
|
+
messages: SSEMessage[];
|
|
14
|
+
isConnected: boolean;
|
|
15
|
+
onStop?: () => void;
|
|
16
|
+
onApprove?: (toolUseId: string, approved: boolean) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatToolName(name: string): string {
|
|
20
|
+
if (!name.startsWith("mcp__")) return name;
|
|
21
|
+
const parts = name.replace(/^mcp__/, "").split("__");
|
|
22
|
+
const service = parts[0] || "";
|
|
23
|
+
const action = (parts[1] || "").replace(/^[^_]+_/, "").replace(/_/g, " ");
|
|
24
|
+
return `${service}: ${action}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function RunOutput({ messages, isConnected, onStop, onApprove }: RunOutputProps) {
|
|
28
|
+
const outputRef = useRef<HTMLDivElement>(null);
|
|
29
|
+
const debugRef = useRef<HTMLPreElement>(null);
|
|
30
|
+
const [showDebug, setShowDebug] = useState(false);
|
|
31
|
+
const [handledApprovals, setHandledApprovals] = useState<Set<string>>(new Set());
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (outputRef.current) outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
|
35
|
+
if (debugRef.current) debugRef.current.scrollTop = debugRef.current.scrollHeight;
|
|
36
|
+
}, [messages]);
|
|
37
|
+
|
|
38
|
+
const tokens = messages.filter((m) => m.type === "token").map((m) => m.text as string).join("");
|
|
39
|
+
|
|
40
|
+
const activities: Array<{ type: string; label: string; key: number }> = [];
|
|
41
|
+
let activityKey = 0;
|
|
42
|
+
for (const m of messages) {
|
|
43
|
+
if (m.type === "thinking" && m.active) activities.push({ type: "thinking", label: "Thinking...", key: activityKey++ });
|
|
44
|
+
else if (m.type === "tool_call" && m.status === "start") activities.push({ type: "tool", label: m.name as string, key: activityKey++ });
|
|
45
|
+
else if (m.type === "progress") activities.push({ type: "progress", label: m.info as string, key: activityKey++ });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const debugMessages = messages.filter((m) => m.type === "debug");
|
|
49
|
+
const doneMsg = messages.find((m) => m.type === "done");
|
|
50
|
+
const stoppedMsg = messages.find((m) => m.type === "stopped");
|
|
51
|
+
const errorMsg = messages.find((m) => m.type === "error" && !doneMsg);
|
|
52
|
+
const statusMsg = messages.findLast((m) => m.type === "status");
|
|
53
|
+
|
|
54
|
+
const pendingApprovals = messages
|
|
55
|
+
.filter((m) => m.type === "approval_required" && !handledApprovals.has(m.tool_use_id as string))
|
|
56
|
+
.map((m) => ({ tool_name: m.tool_name as string, tool_input: m.tool_input as Record<string, unknown>, tool_use_id: m.tool_use_id as string })) as ApprovalRequest[];
|
|
57
|
+
|
|
58
|
+
const handleApproval = (toolUseId: string, approved: boolean) => {
|
|
59
|
+
setHandledApprovals((prev) => new Set(prev).add(toolUseId));
|
|
60
|
+
onApprove?.(toolUseId, approved);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const thinkingMsgs = messages.filter((m) => m.type === "thinking");
|
|
64
|
+
const isThinking = thinkingMsgs.length > 0 && (thinkingMsgs[thinkingMsgs.length - 1].active as boolean);
|
|
65
|
+
const toolMsgs = messages.filter((m) => m.type === "tool_call");
|
|
66
|
+
const lastTool = toolMsgs.length > 0 ? toolMsgs[toolMsgs.length - 1] : null;
|
|
67
|
+
const activeTool = lastTool?.status === "start" ? (lastTool.name as string) : null;
|
|
68
|
+
|
|
69
|
+
if (messages.length === 0 && !isConnected) return null;
|
|
70
|
+
const finishedMsg = doneMsg || stoppedMsg;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="space-y-3">
|
|
74
|
+
<div className="flex items-center gap-2">
|
|
75
|
+
<h3 className="text-[13px] font-medium text-[var(--text-secondary)]">Output</h3>
|
|
76
|
+
{isConnected && (
|
|
77
|
+
<span className="flex items-center gap-1 text-[11px] text-green-400/70">
|
|
78
|
+
<span className="h-1.5 w-1.5 rounded-full bg-green-400/70 animate-pulse" />
|
|
79
|
+
Live
|
|
80
|
+
</span>
|
|
81
|
+
)}
|
|
82
|
+
{finishedMsg && (
|
|
83
|
+
<span className="text-[11px] text-[var(--text-muted)] tabular-nums">
|
|
84
|
+
${(finishedMsg.cost_usd as number)?.toFixed(4) || "?"} · {((finishedMsg.duration_ms as number) / 1000)?.toFixed(1) || "?"}s
|
|
85
|
+
</span>
|
|
86
|
+
)}
|
|
87
|
+
{isConnected && onStop && (
|
|
88
|
+
<button onClick={onStop}
|
|
89
|
+
className="ml-auto rounded-md border border-red-500/20 bg-red-500/10 px-3 py-1 text-[11px] font-medium text-red-400 transition-colors hover:bg-red-500/15">
|
|
90
|
+
Stop
|
|
91
|
+
</button>
|
|
92
|
+
)}
|
|
93
|
+
{debugMessages.length > 0 && (
|
|
94
|
+
<button onClick={() => setShowDebug(!showDebug)}
|
|
95
|
+
className={`${isConnected && onStop ? "" : "ml-auto "}rounded-md bg-[var(--bg-elevated)] px-2 py-0.5 text-[11px] text-[var(--text-tertiary)] hover:bg-[var(--bg-hover)]`}>
|
|
96
|
+
{showDebug ? "Hide" : "Show"} Debug ({debugMessages.length})
|
|
97
|
+
</button>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Approval prompts */}
|
|
102
|
+
{pendingApprovals.map((req) => (
|
|
103
|
+
<div key={req.tool_use_id} className="rounded-md border border-amber-500/15 bg-amber-500/5 p-4 space-y-3">
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
<span className="h-2 w-2 rounded-full bg-amber-400 animate-pulse" />
|
|
106
|
+
<span className="text-[12px] font-medium text-amber-300">Approval Required</span>
|
|
107
|
+
</div>
|
|
108
|
+
<p className="text-[13px] text-[var(--text-secondary)]">
|
|
109
|
+
Run: <span className="font-mono text-amber-200">{formatToolName(req.tool_name)}</span>
|
|
110
|
+
</p>
|
|
111
|
+
{Object.keys(req.tool_input).length > 0 && (
|
|
112
|
+
<pre className="max-h-40 overflow-auto rounded-md border border-[var(--border-subtle)] bg-[var(--bg-base)] p-2 text-[11px] text-[var(--text-muted)] font-mono">
|
|
113
|
+
{JSON.stringify(req.tool_input, null, 2)}
|
|
114
|
+
</pre>
|
|
115
|
+
)}
|
|
116
|
+
<div className="flex gap-2">
|
|
117
|
+
<button onClick={() => handleApproval(req.tool_use_id, true)}
|
|
118
|
+
className="rounded-md bg-green-600/80 px-3 py-1 text-[12px] font-medium text-white transition-colors hover:bg-green-600">
|
|
119
|
+
Allow
|
|
120
|
+
</button>
|
|
121
|
+
<button onClick={() => handleApproval(req.tool_use_id, false)}
|
|
122
|
+
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)]">
|
|
123
|
+
Deny
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
))}
|
|
128
|
+
|
|
129
|
+
{/* Activity bar */}
|
|
130
|
+
{isConnected && (isThinking || activeTool) && (
|
|
131
|
+
<div className="flex items-center gap-2 rounded-md border border-[var(--border-subtle)] bg-[var(--bg-raised)] px-3 py-1.5 text-[11px] text-[var(--text-tertiary)]">
|
|
132
|
+
<span className="h-1.5 w-1.5 rounded-full bg-amber-400 animate-pulse" />
|
|
133
|
+
{isThinking && !activeTool && "Thinking..."}
|
|
134
|
+
{activeTool && <>Using: <span className="font-mono text-amber-300/80">{activeTool}</span></>}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{/* Activity feed */}
|
|
139
|
+
{activities.length > 0 && (
|
|
140
|
+
<div className="flex flex-wrap gap-1.5">
|
|
141
|
+
{activities.map((a) => (
|
|
142
|
+
<span key={a.key}
|
|
143
|
+
className={`inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[10px] font-medium border ${
|
|
144
|
+
a.type === "thinking" ? "bg-[var(--accent-muted)] text-[var(--accent-text)] border-[var(--accent-muted)]"
|
|
145
|
+
: a.type === "tool" ? "bg-amber-500/10 text-amber-400/80 border-amber-500/15"
|
|
146
|
+
: "bg-[var(--bg-elevated)] text-[var(--text-tertiary)] border-[var(--border-subtle)]"
|
|
147
|
+
}`}>
|
|
148
|
+
{a.label}
|
|
149
|
+
</span>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Main output */}
|
|
155
|
+
<div ref={outputRef}
|
|
156
|
+
className="max-h-[500px] overflow-auto rounded-md border border-[var(--border-subtle)] bg-[var(--bg-base)] p-4 text-[13px] text-[var(--text-secondary)] whitespace-pre-wrap break-words">
|
|
157
|
+
{tokens || (statusMsg?.status as string) || "Waiting..."}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{errorMsg && (
|
|
161
|
+
<div className="rounded-md border border-red-500/15 bg-red-500/5 p-3 text-[13px] text-red-400">
|
|
162
|
+
Error: {errorMsg.error as string}
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
{stoppedMsg && (
|
|
166
|
+
<div className="rounded-md border border-amber-500/15 bg-amber-500/5 p-3 text-[13px] text-amber-400/80">
|
|
167
|
+
Stopped by user
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
{doneMsg && !errorMsg && (
|
|
171
|
+
<div className="rounded-md border border-green-500/15 bg-green-500/5 p-3 text-[13px] text-green-400/80">
|
|
172
|
+
Completed successfully
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{showDebug && debugMessages.length > 0 && (
|
|
177
|
+
<div>
|
|
178
|
+
<h4 className="text-[11px] font-medium text-[var(--text-muted)] mb-1">Debug Log</h4>
|
|
179
|
+
<pre ref={debugRef}
|
|
180
|
+
className="max-h-[300px] overflow-auto rounded-md border border-[var(--border-subtle)] bg-[var(--bg-raised)] p-3 text-[11px] text-[var(--text-muted)] whitespace-pre-wrap break-words font-mono">
|
|
181
|
+
{debugMessages.map((m) => m.message as string).join("\n")}
|
|
182
|
+
</pre>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import type { AgentMeta } from "@/lib/types";
|
|
5
|
+
import type { SkillInfo } from "@/lib/capabilities";
|
|
6
|
+
|
|
7
|
+
interface ScheduleFormProps {
|
|
8
|
+
agents: AgentMeta[];
|
|
9
|
+
skills: SkillInfo[];
|
|
10
|
+
onSubmit: (data: {
|
|
11
|
+
agent_slug: string;
|
|
12
|
+
agent_name: string;
|
|
13
|
+
prompt: string;
|
|
14
|
+
cron: string;
|
|
15
|
+
skill_slug?: string;
|
|
16
|
+
}) => void;
|
|
17
|
+
isSubmitting: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const presets = [
|
|
21
|
+
{ label: "Every minute (test)", cron: "* * * * *" },
|
|
22
|
+
{ label: "Every hour", cron: "0 * * * *" },
|
|
23
|
+
{ label: "Daily 9 AM", cron: "0 9 * * *" },
|
|
24
|
+
{ label: "Daily 3 PM", cron: "0 15 * * *" },
|
|
25
|
+
{ label: "Mon-Fri 9 AM", cron: "0 9 * * 1-5" },
|
|
26
|
+
{ label: "Weekly Monday 9 AM", cron: "0 9 * * 1" },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function ScheduleForm({ agents, skills, onSubmit, isSubmitting }: ScheduleFormProps) {
|
|
30
|
+
const [agentSlug, setAgentSlug] = useState("");
|
|
31
|
+
const [skillSlug, setSkillSlug] = useState("");
|
|
32
|
+
const [prompt, setPrompt] = useState("");
|
|
33
|
+
const [cron, setCron] = useState("0 9 * * *");
|
|
34
|
+
|
|
35
|
+
const selectedAgent = agents.find((a) => a.slug === agentSlug);
|
|
36
|
+
const selectedSkill = skills.find((s) => s.slug === skillSlug);
|
|
37
|
+
|
|
38
|
+
const skillsByCategory = skills.reduce<Record<string, SkillInfo[]>>((acc, skill) => {
|
|
39
|
+
(acc[skill.category] ??= []).push(skill);
|
|
40
|
+
return acc;
|
|
41
|
+
}, {});
|
|
42
|
+
|
|
43
|
+
const canSubmit = agentSlug && cron.trim() && (prompt.trim() || skillSlug);
|
|
44
|
+
|
|
45
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
if (!canSubmit) return;
|
|
48
|
+
onSubmit({
|
|
49
|
+
agent_slug: agentSlug,
|
|
50
|
+
agent_name: selectedAgent?.name || agentSlug,
|
|
51
|
+
prompt: prompt.trim(),
|
|
52
|
+
cron: cron.trim(),
|
|
53
|
+
...(skillSlug ? { skill_slug: skillSlug } : {}),
|
|
54
|
+
});
|
|
55
|
+
setPrompt("");
|
|
56
|
+
setSkillSlug("");
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<form onSubmit={handleSubmit} className="space-y-4 rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-raised)] p-5">
|
|
61
|
+
<h3 className="text-[13px] font-medium text-[var(--text-primary)]">New Schedule</h3>
|
|
62
|
+
|
|
63
|
+
<div>
|
|
64
|
+
<label className="block text-[11px] text-[var(--text-tertiary)] mb-1">Agent</label>
|
|
65
|
+
<select
|
|
66
|
+
value={agentSlug}
|
|
67
|
+
onChange={(e) => setAgentSlug(e.target.value)}
|
|
68
|
+
className="w-full rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] px-3 py-2 text-[13px] text-[var(--text-primary)] focus:border-[var(--border-focus)] focus:outline-none"
|
|
69
|
+
>
|
|
70
|
+
<option value="">Select an agent...</option>
|
|
71
|
+
{agents.map((a) => (
|
|
72
|
+
<option key={a.slug} value={a.slug}>{a.emoji} {a.name}</option>
|
|
73
|
+
))}
|
|
74
|
+
</select>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div>
|
|
78
|
+
<label className="block text-[11px] text-[var(--text-tertiary)] mb-1">Skill (optional)</label>
|
|
79
|
+
<select
|
|
80
|
+
value={skillSlug}
|
|
81
|
+
onChange={(e) => setSkillSlug(e.target.value)}
|
|
82
|
+
className="w-full rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] px-3 py-2 text-[13px] text-[var(--text-primary)] focus:border-[var(--border-focus)] focus:outline-none"
|
|
83
|
+
>
|
|
84
|
+
<option value="">No skill — use prompt only</option>
|
|
85
|
+
{Object.entries(skillsByCategory).map(([category, categorySkills]) => (
|
|
86
|
+
<optgroup key={category} label={category}>
|
|
87
|
+
{categorySkills.map((s) => (
|
|
88
|
+
<option key={s.slug} value={s.slug}>{s.name}</option>
|
|
89
|
+
))}
|
|
90
|
+
</optgroup>
|
|
91
|
+
))}
|
|
92
|
+
</select>
|
|
93
|
+
{selectedSkill && (
|
|
94
|
+
<p className="mt-1.5 rounded-md bg-[var(--bg-elevated)] px-3 py-1.5 text-[11px] text-[var(--text-tertiary)]">
|
|
95
|
+
{selectedSkill.description}
|
|
96
|
+
</p>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div>
|
|
101
|
+
<label className="block text-[11px] text-[var(--text-tertiary)] mb-1">
|
|
102
|
+
{skillSlug ? "Additional Instructions (optional)" : "Prompt"}
|
|
103
|
+
</label>
|
|
104
|
+
<textarea
|
|
105
|
+
value={prompt}
|
|
106
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
107
|
+
placeholder={skillSlug ? "Any extra instructions..." : "Enter the prompt for scheduled runs..."}
|
|
108
|
+
rows={2}
|
|
109
|
+
className="w-full rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] px-3 py-2 text-[13px] text-[var(--text-primary)] placeholder-[var(--text-muted)] focus:border-[var(--border-focus)] focus:outline-none"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div>
|
|
114
|
+
<label className="block text-[11px] text-[var(--text-tertiary)] mb-1">Cron Expression</label>
|
|
115
|
+
<input
|
|
116
|
+
value={cron}
|
|
117
|
+
onChange={(e) => setCron(e.target.value)}
|
|
118
|
+
placeholder="0 9 * * *"
|
|
119
|
+
className="w-full rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] px-3 py-2 text-[13px] text-[var(--text-primary)] font-mono placeholder-[var(--text-muted)] focus:border-[var(--border-focus)] focus:outline-none"
|
|
120
|
+
/>
|
|
121
|
+
<div className="mt-2 flex flex-wrap gap-1">
|
|
122
|
+
{presets.map((p) => (
|
|
123
|
+
<button
|
|
124
|
+
key={p.cron}
|
|
125
|
+
type="button"
|
|
126
|
+
onClick={() => setCron(p.cron)}
|
|
127
|
+
className={`rounded-md px-2 py-0.5 text-[11px] transition-colors ${
|
|
128
|
+
cron === p.cron
|
|
129
|
+
? "bg-[var(--accent)] text-white"
|
|
130
|
+
: "bg-[var(--bg-elevated)] text-[var(--text-tertiary)] hover:bg-[var(--bg-hover)]"
|
|
131
|
+
}`}
|
|
132
|
+
>
|
|
133
|
+
{p.label}
|
|
134
|
+
</button>
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<button
|
|
140
|
+
type="submit"
|
|
141
|
+
disabled={!canSubmit || isSubmitting}
|
|
142
|
+
className="rounded-md bg-[var(--accent)] px-4 py-2 text-[12px] font-medium text-white transition-colors hover:bg-[var(--accent-hover)] disabled:opacity-50 disabled:cursor-not-allowed"
|
|
143
|
+
>
|
|
144
|
+
{isSubmitting ? "Creating..." : "Create Schedule"}
|
|
145
|
+
</button>
|
|
146
|
+
</form>
|
|
147
|
+
);
|
|
148
|
+
}
|