claude-smart 0.2.22 → 0.2.24
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/.agents/plugins/marketplace.json +20 -0
- package/README.md +69 -27
- package/bin/claude-smart.js +296 -11
- package/package.json +11 -1
- package/plugin/.claude-plugin/plugin.json +17 -0
- package/plugin/.codex-plugin/plugin.json +35 -0
- package/plugin/LICENSE +202 -0
- package/plugin/README.md +37 -0
- package/plugin/bin/cs-cite +77 -0
- package/plugin/commands/clear-all.md +8 -0
- package/plugin/commands/dashboard.md +8 -0
- package/plugin/commands/learn.md +12 -0
- package/plugin/commands/restart.md +8 -0
- package/plugin/commands/show.md +8 -0
- package/plugin/dashboard/AGENTS.md +6 -0
- package/plugin/dashboard/app/api/claude-settings/route.ts +19 -0
- package/plugin/dashboard/app/api/config/route.ts +16 -0
- package/plugin/dashboard/app/api/health/route.ts +10 -0
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +63 -0
- package/plugin/dashboard/app/api/sessions/[id]/route.ts +28 -0
- package/plugin/dashboard/app/api/sessions/route.ts +14 -0
- package/plugin/dashboard/app/configure/env/page.tsx +318 -0
- package/plugin/dashboard/app/configure/layout.tsx +47 -0
- package/plugin/dashboard/app/configure/page.tsx +5 -0
- package/plugin/dashboard/app/configure/server/page.tsx +258 -0
- package/plugin/dashboard/app/dashboard/page.tsx +227 -0
- package/plugin/dashboard/app/globals.css +129 -0
- package/plugin/dashboard/app/icon.png +0 -0
- package/plugin/dashboard/app/layout.tsx +40 -0
- package/plugin/dashboard/app/page.tsx +5 -0
- package/plugin/dashboard/app/preferences/[id]/page.tsx +531 -0
- package/plugin/dashboard/app/preferences/page.tsx +126 -0
- package/plugin/dashboard/app/providers.tsx +12 -0
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +321 -0
- package/plugin/dashboard/app/sessions/page.tsx +186 -0
- package/plugin/dashboard/app/skills/page.tsx +362 -0
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +597 -0
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +830 -0
- package/plugin/dashboard/components/common/delete-all-button.tsx +45 -0
- package/plugin/dashboard/components/common/empty-state.tsx +34 -0
- package/plugin/dashboard/components/common/learnings-badge.tsx +34 -0
- package/plugin/dashboard/components/common/page-header.tsx +34 -0
- package/plugin/dashboard/components/common/page-tabs.tsx +115 -0
- package/plugin/dashboard/components/common/stat-card.tsx +38 -0
- package/plugin/dashboard/components/layout/nav-items.ts +22 -0
- package/plugin/dashboard/components/layout/sidebar.tsx +45 -0
- package/plugin/dashboard/components/layout/top-bar.tsx +64 -0
- package/plugin/dashboard/components/stall-banner.tsx +53 -0
- package/plugin/dashboard/components/ui/badge.tsx +52 -0
- package/plugin/dashboard/components/ui/button.tsx +60 -0
- package/plugin/dashboard/components/ui/collapsible.tsx +21 -0
- package/plugin/dashboard/components/ui/input.tsx +20 -0
- package/plugin/dashboard/components/ui/label.tsx +20 -0
- package/plugin/dashboard/components/ui/scroll-area.tsx +55 -0
- package/plugin/dashboard/components/ui/select.tsx +201 -0
- package/plugin/dashboard/components/ui/separator.tsx +25 -0
- package/plugin/dashboard/components/ui/sheet.tsx +135 -0
- package/plugin/dashboard/components/ui/switch.tsx +32 -0
- package/plugin/dashboard/components.json +25 -0
- package/plugin/dashboard/eslint.config.mjs +16 -0
- package/plugin/dashboard/hooks/use-settings.tsx +88 -0
- package/plugin/dashboard/hooks/use-stall-state.ts +59 -0
- package/plugin/dashboard/lib/claude-settings-file.ts +114 -0
- package/plugin/dashboard/lib/config-file.ts +131 -0
- package/plugin/dashboard/lib/format.ts +58 -0
- package/plugin/dashboard/lib/reflexio-client.ts +238 -0
- package/plugin/dashboard/lib/reflexio-url.ts +17 -0
- package/plugin/dashboard/lib/session-reader.ts +245 -0
- package/plugin/dashboard/lib/status.ts +24 -0
- package/plugin/dashboard/lib/types.ts +145 -0
- package/plugin/dashboard/lib/utils.ts +6 -0
- package/plugin/dashboard/next.config.ts +7 -0
- package/plugin/dashboard/package-lock.json +10275 -0
- package/plugin/dashboard/package.json +37 -0
- package/plugin/dashboard/postcss.config.mjs +7 -0
- package/plugin/dashboard/public/claude-smart-icon.png +0 -0
- package/plugin/dashboard/tsconfig.json +34 -0
- package/plugin/hooks/codex-hooks.json +67 -0
- package/plugin/hooks/hooks.json +111 -0
- package/plugin/pyproject.toml +49 -0
- package/plugin/scripts/_codex_env.sh +27 -0
- package/plugin/scripts/_lib.sh +325 -0
- package/plugin/scripts/backend-service.sh +208 -0
- package/plugin/scripts/cli.sh +40 -0
- package/plugin/scripts/dashboard-build.sh +139 -0
- package/plugin/scripts/dashboard-open.sh +107 -0
- package/plugin/scripts/dashboard-service.sh +195 -0
- package/plugin/scripts/ensure-plugin-root.sh +84 -0
- package/plugin/scripts/hook_entry.sh +70 -0
- package/plugin/scripts/smart-install.sh +411 -0
- package/plugin/src/claude_smart/__init__.py +3 -0
- package/plugin/src/claude_smart/cli.py +1273 -0
- package/plugin/src/claude_smart/context_format.py +277 -0
- package/plugin/src/claude_smart/context_inject.py +92 -0
- package/plugin/src/claude_smart/cs_cite.py +236 -0
- package/plugin/src/claude_smart/events/__init__.py +1 -0
- package/plugin/src/claude_smart/events/post_tool.py +148 -0
- package/plugin/src/claude_smart/events/pre_tool.py +52 -0
- package/plugin/src/claude_smart/events/session_end.py +20 -0
- package/plugin/src/claude_smart/events/session_start.py +119 -0
- package/plugin/src/claude_smart/events/stop.py +393 -0
- package/plugin/src/claude_smart/events/user_prompt.py +73 -0
- package/plugin/src/claude_smart/hook.py +114 -0
- package/plugin/src/claude_smart/ids.py +56 -0
- package/plugin/src/claude_smart/internal_call.py +89 -0
- package/plugin/src/claude_smart/optimizer_assistant.py +203 -0
- package/plugin/src/claude_smart/publish.py +71 -0
- package/plugin/src/claude_smart/query_compose.py +51 -0
- package/plugin/src/claude_smart/reflexio_adapter.py +403 -0
- package/plugin/src/claude_smart/runtime.py +52 -0
- package/plugin/src/claude_smart/stall_banner.py +61 -0
- package/plugin/src/claude_smart/state.py +276 -0
- package/plugin/uv.lock +3720 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Trash2 } from "lucide-react";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
label: string;
|
|
9
|
+
confirmMessage: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
onConfirm: () => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DeleteAllButton({
|
|
15
|
+
label,
|
|
16
|
+
confirmMessage,
|
|
17
|
+
disabled,
|
|
18
|
+
onConfirm,
|
|
19
|
+
}: Props) {
|
|
20
|
+
const [busy, setBusy] = useState(false);
|
|
21
|
+
|
|
22
|
+
const click = async () => {
|
|
23
|
+
if (busy) return;
|
|
24
|
+
if (!confirm(confirmMessage)) return;
|
|
25
|
+
setBusy(true);
|
|
26
|
+
try {
|
|
27
|
+
await onConfirm();
|
|
28
|
+
} finally {
|
|
29
|
+
setBusy(false);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Button
|
|
35
|
+
variant="outline"
|
|
36
|
+
size="sm"
|
|
37
|
+
onClick={click}
|
|
38
|
+
disabled={busy || disabled}
|
|
39
|
+
className="text-destructive hover:bg-destructive/10 hover:text-destructive border-destructive/30"
|
|
40
|
+
>
|
|
41
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
42
|
+
{busy ? "Deleting…" : label}
|
|
43
|
+
</Button>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
import type { LucideIcon } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
export function EmptyState({
|
|
5
|
+
icon: Icon,
|
|
6
|
+
title,
|
|
7
|
+
description,
|
|
8
|
+
action,
|
|
9
|
+
className,
|
|
10
|
+
}: {
|
|
11
|
+
icon?: LucideIcon;
|
|
12
|
+
title: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
action?: React.ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={cn(
|
|
20
|
+
"flex flex-col items-center justify-center text-center py-12 px-6 border border-dashed border-border rounded-xl bg-muted/20",
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
>
|
|
24
|
+
{Icon && (
|
|
25
|
+
<Icon className="h-8 w-8 text-muted-foreground/60 mb-3" strokeWidth={1.5} />
|
|
26
|
+
)}
|
|
27
|
+
<div className="text-sm font-medium text-foreground">{title}</div>
|
|
28
|
+
{description && (
|
|
29
|
+
<p className="text-sm text-muted-foreground mt-1 max-w-md">{description}</p>
|
|
30
|
+
)}
|
|
31
|
+
{action && <div className="mt-4">{action}</div>}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Sparkles } from "lucide-react";
|
|
2
|
+
import { Badge } from "@/components/ui/badge";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export function LearningsBadge({
|
|
6
|
+
count,
|
|
7
|
+
size = "md",
|
|
8
|
+
className,
|
|
9
|
+
}: {
|
|
10
|
+
count: number;
|
|
11
|
+
size?: "md" | "sm";
|
|
12
|
+
className?: string;
|
|
13
|
+
}) {
|
|
14
|
+
if (count <= 0) return null;
|
|
15
|
+
const isSmall = size === "sm";
|
|
16
|
+
return (
|
|
17
|
+
<Badge
|
|
18
|
+
variant="outline"
|
|
19
|
+
className={cn(
|
|
20
|
+
"border-amber-500/40 gap-1",
|
|
21
|
+
isSmall ? "h-4 px-1.5 text-[10px] shrink-0" : "h-5",
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
>
|
|
25
|
+
<Sparkles
|
|
26
|
+
className={cn(
|
|
27
|
+
"text-amber-500",
|
|
28
|
+
isSmall ? "h-2.5 w-2.5" : "h-3 w-3",
|
|
29
|
+
)}
|
|
30
|
+
/>
|
|
31
|
+
{count} learning applied
|
|
32
|
+
</Badge>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
|
|
3
|
+
export function PageHeader({
|
|
4
|
+
title,
|
|
5
|
+
description,
|
|
6
|
+
actions,
|
|
7
|
+
className,
|
|
8
|
+
}: {
|
|
9
|
+
title: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
actions?: React.ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
className={cn(
|
|
17
|
+
"border-b border-border px-6 py-5 flex flex-wrap items-start justify-between gap-4",
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
<div className="min-w-[min(18rem,100%)] flex-1">
|
|
22
|
+
<h1 className="text-xl font-semibold tracking-tight">{title}</h1>
|
|
23
|
+
{description && (
|
|
24
|
+
<p className="text-sm text-muted-foreground mt-0.5">{description}</p>
|
|
25
|
+
)}
|
|
26
|
+
</div>
|
|
27
|
+
{actions && (
|
|
28
|
+
<div className="flex max-w-full flex-wrap items-center justify-end gap-2">
|
|
29
|
+
{actions}
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import type { LucideIcon } from "lucide-react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
export interface PageTabItem {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
count?: number;
|
|
12
|
+
href?: string;
|
|
13
|
+
icon?: LucideIcon;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface PageTabsProps {
|
|
17
|
+
items: PageTabItem[];
|
|
18
|
+
activeId: string;
|
|
19
|
+
onSelect?: (id: string) => void;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function PageTabs({
|
|
24
|
+
items,
|
|
25
|
+
activeId,
|
|
26
|
+
onSelect,
|
|
27
|
+
className,
|
|
28
|
+
}: PageTabsProps) {
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
className={cn(
|
|
32
|
+
"grid gap-2 sm:grid-cols-[repeat(auto-fit,minmax(220px,1fr))]",
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
role="tablist"
|
|
36
|
+
>
|
|
37
|
+
{items.map((item) => {
|
|
38
|
+
const active = item.id === activeId;
|
|
39
|
+
const Icon = item.icon;
|
|
40
|
+
const content = (
|
|
41
|
+
<>
|
|
42
|
+
<span className="flex items-start justify-between gap-3">
|
|
43
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
44
|
+
{Icon && (
|
|
45
|
+
<Icon
|
|
46
|
+
className={cn(
|
|
47
|
+
"mt-0.5 h-4 w-4 shrink-0",
|
|
48
|
+
active ? "text-foreground" : "text-muted-foreground",
|
|
49
|
+
)}
|
|
50
|
+
/>
|
|
51
|
+
)}
|
|
52
|
+
<span className="truncate text-sm font-semibold">
|
|
53
|
+
{item.label}
|
|
54
|
+
</span>
|
|
55
|
+
</span>
|
|
56
|
+
{item.count !== undefined && (
|
|
57
|
+
<span
|
|
58
|
+
className={cn(
|
|
59
|
+
"rounded-md border px-1.5 py-0.5 font-mono text-[10px]",
|
|
60
|
+
active
|
|
61
|
+
? "border-foreground/20 bg-background text-foreground"
|
|
62
|
+
: "border-border bg-muted/40 text-muted-foreground",
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
{item.count}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
</span>
|
|
69
|
+
{item.description && (
|
|
70
|
+
<span className="mt-1 block text-xs leading-relaxed text-muted-foreground">
|
|
71
|
+
{item.description}
|
|
72
|
+
</span>
|
|
73
|
+
)}
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const className = cn(
|
|
78
|
+
"relative rounded-lg border px-3 py-2.5 text-left transition-colors",
|
|
79
|
+
"hover:border-foreground/25 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50",
|
|
80
|
+
active
|
|
81
|
+
? "border-foreground/25 bg-card text-foreground shadow-sm before:absolute before:inset-x-3 before:top-0 before:h-0.5 before:rounded-full before:bg-foreground"
|
|
82
|
+
: "border-border bg-background/60 text-muted-foreground",
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (item.href) {
|
|
86
|
+
return (
|
|
87
|
+
<Link
|
|
88
|
+
key={item.id}
|
|
89
|
+
href={item.href}
|
|
90
|
+
className={className}
|
|
91
|
+
role="tab"
|
|
92
|
+
aria-selected={active}
|
|
93
|
+
aria-current={active ? "page" : undefined}
|
|
94
|
+
>
|
|
95
|
+
{content}
|
|
96
|
+
</Link>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<button
|
|
102
|
+
key={item.id}
|
|
103
|
+
type="button"
|
|
104
|
+
className={className}
|
|
105
|
+
role="tab"
|
|
106
|
+
aria-selected={active}
|
|
107
|
+
onClick={() => onSelect?.(item.id)}
|
|
108
|
+
>
|
|
109
|
+
{content}
|
|
110
|
+
</button>
|
|
111
|
+
);
|
|
112
|
+
})}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
import type { LucideIcon } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
export function StatCard({
|
|
5
|
+
label,
|
|
6
|
+
value,
|
|
7
|
+
hint,
|
|
8
|
+
icon: Icon,
|
|
9
|
+
className,
|
|
10
|
+
}: {
|
|
11
|
+
label: string;
|
|
12
|
+
value: string | number;
|
|
13
|
+
hint?: string;
|
|
14
|
+
icon?: LucideIcon;
|
|
15
|
+
className?: string;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={cn(
|
|
20
|
+
"rounded-xl border border-border bg-card px-5 py-4 flex items-start justify-between gap-4",
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
>
|
|
24
|
+
<div className="min-w-0">
|
|
25
|
+
<div className="text-xs uppercase tracking-wide text-muted-foreground font-medium">
|
|
26
|
+
{label}
|
|
27
|
+
</div>
|
|
28
|
+
<div className="mt-1.5 text-2xl font-semibold tracking-tight tabular-nums">
|
|
29
|
+
{value}
|
|
30
|
+
</div>
|
|
31
|
+
{hint && (
|
|
32
|
+
<div className="text-xs text-muted-foreground mt-1">{hint}</div>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
{Icon && <Icon className="h-4 w-4 text-muted-foreground mt-0.5" />}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LayoutDashboard,
|
|
3
|
+
MessageSquare,
|
|
4
|
+
Users,
|
|
5
|
+
BookOpen,
|
|
6
|
+
Settings,
|
|
7
|
+
type LucideIcon,
|
|
8
|
+
} from "lucide-react";
|
|
9
|
+
|
|
10
|
+
export interface NavItem {
|
|
11
|
+
href: string;
|
|
12
|
+
label: string;
|
|
13
|
+
icon: LucideIcon;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const navItems: NavItem[] = [
|
|
17
|
+
{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
|
|
18
|
+
{ href: "/sessions", label: "Sessions", icon: MessageSquare },
|
|
19
|
+
{ href: "/preferences", label: "Preferences", icon: Users },
|
|
20
|
+
{ href: "/skills", label: "Skills", icon: BookOpen },
|
|
21
|
+
{ href: "/configure", label: "Configure", icon: Settings },
|
|
22
|
+
];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
7
|
+
import { navItems } from "./nav-items";
|
|
8
|
+
|
|
9
|
+
export function Sidebar() {
|
|
10
|
+
const pathname = usePathname();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<ScrollArea className="h-full">
|
|
14
|
+
<div className="px-3 py-4">
|
|
15
|
+
<div className="px-2 mb-6">
|
|
16
|
+
<div className="font-semibold text-sm">Claude-Smart</div>
|
|
17
|
+
<div className="text-[11px] text-muted-foreground font-mono">dashboard</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<nav className="space-y-0.5">
|
|
21
|
+
{navItems.map((item) => {
|
|
22
|
+
const active =
|
|
23
|
+
pathname === item.href || pathname.startsWith(`${item.href}/`);
|
|
24
|
+
const Icon = item.icon;
|
|
25
|
+
return (
|
|
26
|
+
<Link
|
|
27
|
+
key={item.href}
|
|
28
|
+
href={item.href}
|
|
29
|
+
className={cn(
|
|
30
|
+
"flex items-center gap-2 px-2 py-1.5 text-sm rounded-md transition-colors",
|
|
31
|
+
active
|
|
32
|
+
? "bg-accent text-accent-foreground font-medium"
|
|
33
|
+
: "text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
<Icon className="h-4 w-4 shrink-0" />
|
|
37
|
+
<span className="truncate">{item.label}</span>
|
|
38
|
+
</Link>
|
|
39
|
+
);
|
|
40
|
+
})}
|
|
41
|
+
</nav>
|
|
42
|
+
</div>
|
|
43
|
+
</ScrollArea>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTheme } from "next-themes";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { Moon, Sun, Menu } from "lucide-react";
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import { Button } from "@/components/ui/button";
|
|
8
|
+
import { useSettings } from "@/hooks/use-settings";
|
|
9
|
+
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
|
10
|
+
import { Sidebar } from "./sidebar";
|
|
11
|
+
|
|
12
|
+
export function TopBar() {
|
|
13
|
+
const { reflexioUrl, setReflexioUrl } = useSettings();
|
|
14
|
+
const { theme, setTheme } = useTheme();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<header className="h-14 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 flex items-center px-4 gap-3 shrink-0">
|
|
18
|
+
<Sheet>
|
|
19
|
+
<SheetTrigger
|
|
20
|
+
render={<Button variant="ghost" size="icon" className="lg:hidden" />}
|
|
21
|
+
>
|
|
22
|
+
<Menu className="h-4 w-4" />
|
|
23
|
+
</SheetTrigger>
|
|
24
|
+
<SheetContent side="left" className="w-72 p-0">
|
|
25
|
+
<Sidebar />
|
|
26
|
+
</SheetContent>
|
|
27
|
+
</Sheet>
|
|
28
|
+
|
|
29
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
30
|
+
<Image
|
|
31
|
+
src="/claude-smart-icon.png"
|
|
32
|
+
alt="claude-smart"
|
|
33
|
+
width={24}
|
|
34
|
+
height={24}
|
|
35
|
+
className="h-6 w-6 shrink-0"
|
|
36
|
+
priority
|
|
37
|
+
/>
|
|
38
|
+
<span className="text-sm font-semibold whitespace-nowrap hidden sm:block">
|
|
39
|
+
Claude-Smart
|
|
40
|
+
</span>
|
|
41
|
+
<div className="mx-2 h-5 w-px bg-border hidden sm:block" />
|
|
42
|
+
<label className="text-xs text-muted-foreground whitespace-nowrap hidden sm:block">
|
|
43
|
+
Reflexio
|
|
44
|
+
</label>
|
|
45
|
+
<Input
|
|
46
|
+
value={reflexioUrl}
|
|
47
|
+
onChange={(e) => setReflexioUrl(e.target.value)}
|
|
48
|
+
placeholder="http://localhost:8071"
|
|
49
|
+
className="h-8 text-xs max-w-xs font-mono"
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<Button
|
|
54
|
+
variant="ghost"
|
|
55
|
+
size="icon"
|
|
56
|
+
className="h-8 w-8"
|
|
57
|
+
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
58
|
+
>
|
|
59
|
+
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
60
|
+
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
61
|
+
</Button>
|
|
62
|
+
</header>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useStallState } from "@/hooks/use-stall-state";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Persistent top-of-page banner shown while reflexio learning is stalled.
|
|
7
|
+
* Renders nothing when state is null or clean.
|
|
8
|
+
*/
|
|
9
|
+
export function StallBanner() {
|
|
10
|
+
const state = useStallState();
|
|
11
|
+
if (!state || !state.stalled) return null;
|
|
12
|
+
|
|
13
|
+
const text = renderText(state.reason, state.reset_estimate);
|
|
14
|
+
if (!text) return null;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
role="status"
|
|
19
|
+
aria-live="polite"
|
|
20
|
+
className="w-full bg-amber-100 text-amber-900 border-b border-amber-300 px-4 py-2 text-sm dark:bg-amber-950 dark:text-amber-100 dark:border-amber-800"
|
|
21
|
+
>
|
|
22
|
+
{text}
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderText(
|
|
28
|
+
reason: string | null,
|
|
29
|
+
resetEstimate: string | null,
|
|
30
|
+
): string {
|
|
31
|
+
if (reason === "billing_error") {
|
|
32
|
+
if (resetEstimate) {
|
|
33
|
+
const formatted = formatReset(resetEstimate);
|
|
34
|
+
return `claude-smart: learning paused — Agent SDK credit exhausted (resets ~${formatted}).`;
|
|
35
|
+
}
|
|
36
|
+
return "claude-smart: learning paused — Agent SDK credit exhausted.";
|
|
37
|
+
}
|
|
38
|
+
if (reason === "auth_error") {
|
|
39
|
+
return "claude-smart: learning paused — please run /login in Claude Code.";
|
|
40
|
+
}
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatReset(iso: string): string {
|
|
45
|
+
const d = new Date(iso);
|
|
46
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
47
|
+
return d.toLocaleString(undefined, {
|
|
48
|
+
month: "short",
|
|
49
|
+
day: "numeric",
|
|
50
|
+
hour: "2-digit",
|
|
51
|
+
minute: "2-digit",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props"
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
13
|
+
secondary:
|
|
14
|
+
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
15
|
+
destructive:
|
|
16
|
+
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
|
17
|
+
outline:
|
|
18
|
+
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
19
|
+
ghost:
|
|
20
|
+
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: "default",
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
function Badge({
|
|
31
|
+
className,
|
|
32
|
+
variant = "default",
|
|
33
|
+
render,
|
|
34
|
+
...props
|
|
35
|
+
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
|
36
|
+
return useRender({
|
|
37
|
+
defaultTagName: "span",
|
|
38
|
+
props: mergeProps<"span">(
|
|
39
|
+
{
|
|
40
|
+
className: cn(badgeVariants({ variant }), className),
|
|
41
|
+
},
|
|
42
|
+
props
|
|
43
|
+
),
|
|
44
|
+
render,
|
|
45
|
+
state: {
|
|
46
|
+
slot: "badge",
|
|
47
|
+
variant,
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { Badge, badgeVariants }
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
const buttonVariants = cva(
|
|
9
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
14
|
+
outline:
|
|
15
|
+
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
16
|
+
secondary:
|
|
17
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
18
|
+
ghost:
|
|
19
|
+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
20
|
+
destructive:
|
|
21
|
+
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
|
22
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
23
|
+
},
|
|
24
|
+
size: {
|
|
25
|
+
default:
|
|
26
|
+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
27
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
28
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
29
|
+
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
|
30
|
+
icon: "size-8",
|
|
31
|
+
"icon-xs":
|
|
32
|
+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
33
|
+
"icon-sm":
|
|
34
|
+
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
35
|
+
"icon-lg": "size-9",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
defaultVariants: {
|
|
39
|
+
variant: "default",
|
|
40
|
+
size: "default",
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
function Button({
|
|
46
|
+
className,
|
|
47
|
+
variant = "default",
|
|
48
|
+
size = "default",
|
|
49
|
+
...props
|
|
50
|
+
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
|
51
|
+
return (
|
|
52
|
+
<ButtonPrimitive
|
|
53
|
+
data-slot="button"
|
|
54
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"
|
|
4
|
+
|
|
5
|
+
function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
|
|
6
|
+
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
|
|
10
|
+
return (
|
|
11
|
+
<CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
|
|
16
|
+
return (
|
|
17
|
+
<CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Input as InputPrimitive } from "@base-ui/react/input"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
7
|
+
return (
|
|
8
|
+
<InputPrimitive
|
|
9
|
+
type={type}
|
|
10
|
+
data-slot="input"
|
|
11
|
+
className={cn(
|
|
12
|
+
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { Input }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
|
8
|
+
return (
|
|
9
|
+
<label
|
|
10
|
+
data-slot="label"
|
|
11
|
+
className={cn(
|
|
12
|
+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { Label }
|