ccnew 0.1.10
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/README.md +107 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/core/apply.js +152 -0
- package/core/backup.js +53 -0
- package/core/constants.js +78 -0
- package/core/desktop-service.js +403 -0
- package/core/desktop-state.js +1021 -0
- package/core/index.js +1468 -0
- package/core/paths.js +99 -0
- package/core/presets.js +171 -0
- package/core/probe.js +70 -0
- package/core/routing.js +334 -0
- package/core/store.js +218 -0
- package/core/utils.js +225 -0
- package/core/writers/codex.js +102 -0
- package/core/writers/index.js +16 -0
- package/core/writers/openclaw.js +93 -0
- package/core/writers/opencode.js +91 -0
- package/desktop/assets/fml-icon.png +0 -0
- package/desktop/assets/march-mark.svg +26 -0
- package/desktop/main.js +275 -0
- package/desktop/preload.cjs +67 -0
- package/desktop/preload.js +49 -0
- package/desktop/renderer/app.js +327 -0
- package/desktop/renderer/index.html +130 -0
- package/desktop/renderer/styles.css +490 -0
- package/package.json +111 -0
- package/scripts/build-web.mjs +95 -0
- package/scripts/desktop-dev.mjs +90 -0
- package/scripts/desktop-pack-win.mjs +81 -0
- package/scripts/postinstall.mjs +49 -0
- package/scripts/prepublish-check.mjs +57 -0
- package/scripts/serve-site.mjs +51 -0
- package/site/app.js +10 -0
- package/site/assets/fml-icon.png +0 -0
- package/site/assets/march-mark.svg +26 -0
- package/site/index.html +337 -0
- package/site/styles.css +840 -0
- package/src/App.tsx +1557 -0
- package/src/components/layout/app-sidebar.tsx +103 -0
- package/src/components/layout/top-toolbar.tsx +44 -0
- package/src/components/layout/workspace-tabs.tsx +32 -0
- package/src/components/providers/inspector-panel.tsx +84 -0
- package/src/components/providers/metric-strip.tsx +26 -0
- package/src/components/providers/provider-editor.tsx +87 -0
- package/src/components/providers/provider-table.tsx +85 -0
- package/src/components/ui/logo-mark.tsx +32 -0
- package/src/features/mcp/mcp-view.tsx +45 -0
- package/src/features/prompts/prompts-view.tsx +40 -0
- package/src/features/providers/providers-view.tsx +40 -0
- package/src/features/providers/types.ts +26 -0
- package/src/features/skills/skills-view.tsx +44 -0
- package/src/hooks/use-control-workspace.ts +235 -0
- package/src/index.css +22 -0
- package/src/lib/client.ts +726 -0
- package/src/lib/query-client.ts +3 -0
- package/src/lib/workspace-sections.ts +34 -0
- package/src/main.tsx +14 -0
- package/src/types.ts +137 -0
- package/src/vite-env.d.ts +64 -0
- package/src-tauri/README.md +11 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { Bot, Boxes, ChevronRight, Search, Settings2, ShieldCheck, Sparkles, TerminalSquare } from "lucide-react";
|
|
3
|
+
import type { AppSnapshot, PlatformId } from "../../types";
|
|
4
|
+
import { LogoMark } from "../ui/logo-mark";
|
|
5
|
+
|
|
6
|
+
const platformIcons = {
|
|
7
|
+
codex: Bot,
|
|
8
|
+
opencode: Boxes,
|
|
9
|
+
openclaw: ShieldCheck
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function AppSidebar(props: {
|
|
13
|
+
snapshot: AppSnapshot;
|
|
14
|
+
activePlatform: PlatformId;
|
|
15
|
+
onPlatformChange: (platform: PlatformId) => void;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<aside className="glass flex w-[252px] shrink-0 flex-col rounded-[28px] border border-white/6 px-4 py-5 shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
|
|
19
|
+
<div className="flex items-center gap-3 px-2">
|
|
20
|
+
<LogoMark />
|
|
21
|
+
<div>
|
|
22
|
+
<div className="text-[11px] font-semibold uppercase tracking-[0.34em] text-slate-500">FHL</div>
|
|
23
|
+
<div className="font-['Space_Grotesk'] text-xl font-bold text-slate-50">Control</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div className="mt-6 rounded-2xl border border-white/6 bg-white/[0.02] px-3 py-2">
|
|
28
|
+
<div className="flex items-center gap-2 text-sm text-slate-400">
|
|
29
|
+
<Search className="h-4 w-4" />
|
|
30
|
+
<span>Search providers / routes / models</span>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className="mt-6">
|
|
35
|
+
<div className="px-2 text-[11px] uppercase tracking-[0.3em] text-slate-500">Clients</div>
|
|
36
|
+
<div className="mt-2 space-y-2">
|
|
37
|
+
{props.snapshot.platforms.map((item) => {
|
|
38
|
+
const Icon = platformIcons[item.id];
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<button
|
|
42
|
+
key={item.id}
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={() => props.onPlatformChange(item.id)}
|
|
45
|
+
className={clsx(
|
|
46
|
+
"flex w-full items-center gap-3 rounded-2xl border px-3 py-3 text-left transition",
|
|
47
|
+
item.id === props.activePlatform
|
|
48
|
+
? "border-cyan-300/20 bg-cyan-400/[0.08]"
|
|
49
|
+
: "border-white/6 bg-white/[0.02] hover:bg-white/[0.045]"
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
<div
|
|
53
|
+
className={clsx(
|
|
54
|
+
"rounded-xl p-2",
|
|
55
|
+
item.id === props.activePlatform ? "bg-cyan-400/12 text-cyan-200" : "bg-slate-900/70 text-slate-400"
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
<Icon className="h-4 w-4" />
|
|
59
|
+
</div>
|
|
60
|
+
<div className="min-w-0 flex-1">
|
|
61
|
+
<div className="font-medium text-slate-100">{item.label}</div>
|
|
62
|
+
<div className="truncate text-xs text-slate-500">{item.providerCount} saved routes</div>
|
|
63
|
+
</div>
|
|
64
|
+
<ChevronRight className="h-4 w-4 text-slate-600" />
|
|
65
|
+
</button>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className="mt-6">
|
|
72
|
+
<div className="px-2 text-[11px] uppercase tracking-[0.3em] text-slate-500">Modules</div>
|
|
73
|
+
<div className="mt-2 space-y-2">
|
|
74
|
+
<div className="rounded-2xl border border-white/6 bg-white/[0.02] px-4 py-3">
|
|
75
|
+
<div className="flex items-center gap-2 text-sm text-slate-200">
|
|
76
|
+
<TerminalSquare className="h-4 w-4 text-cyan-300" />
|
|
77
|
+
Provider Workspace
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="rounded-2xl border border-white/6 bg-white/[0.02] px-4 py-3">
|
|
81
|
+
<div className="flex items-center gap-2 text-sm text-slate-200">
|
|
82
|
+
<Sparkles className="h-4 w-4 text-slate-300" />
|
|
83
|
+
MCP / Prompts / Skills
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div className="rounded-2xl border border-white/6 bg-white/[0.02] px-4 py-3">
|
|
87
|
+
<div className="flex items-center gap-2 text-sm text-slate-200">
|
|
88
|
+
<Settings2 className="h-4 w-4 text-slate-300" />
|
|
89
|
+
Desktop Settings
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div className="mt-auto rounded-[24px] border border-cyan-300/10 bg-gradient-to-br from-cyan-400/[0.07] to-teal-400/[0.05] p-4">
|
|
96
|
+
<div className="text-[11px] uppercase tracking-[0.3em] text-cyan-200/65">Visual Target</div>
|
|
97
|
+
<div className="mt-3 text-sm font-medium leading-6 text-slate-200">
|
|
98
|
+
CC Switch-style structure and density, with your own name and logo only.
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</aside>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function TopToolbar(props: {
|
|
2
|
+
title: string;
|
|
3
|
+
currentRoute: string | null;
|
|
4
|
+
onRefresh: () => void;
|
|
5
|
+
onProbe: () => void;
|
|
6
|
+
}) {
|
|
7
|
+
return (
|
|
8
|
+
<header className="flex items-center justify-between gap-4 border-b border-white/6 pb-5">
|
|
9
|
+
<div>
|
|
10
|
+
<div className="text-[11px] uppercase tracking-[0.34em] text-slate-500">Desktop Control Center</div>
|
|
11
|
+
<h1 className="mt-2 font-['Space_Grotesk'] text-[38px] font-bold tracking-[-0.05em] text-slate-50">
|
|
12
|
+
{props.title}
|
|
13
|
+
</h1>
|
|
14
|
+
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-400">
|
|
15
|
+
Closer to the CC Switch control-surface layout, with denser modules and utility-first hierarchy.
|
|
16
|
+
</p>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div className="flex items-center gap-3">
|
|
20
|
+
<button className="rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">MCP</button>
|
|
21
|
+
<button className="rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">Prompts</button>
|
|
22
|
+
<button className="rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">Skills</button>
|
|
23
|
+
<div className="rounded-2xl border border-emerald-400/10 bg-emerald-400/[0.05] px-4 py-3">
|
|
24
|
+
<div className="text-[11px] uppercase tracking-[0.28em] text-emerald-300/70">Current Route</div>
|
|
25
|
+
<div className="mt-1 text-sm font-medium text-emerald-200">{props.currentRoute || "No Active Route"}</div>
|
|
26
|
+
</div>
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
onClick={props.onRefresh}
|
|
30
|
+
className="rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-3 text-sm text-slate-200"
|
|
31
|
+
>
|
|
32
|
+
Refresh
|
|
33
|
+
</button>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={props.onProbe}
|
|
37
|
+
className="rounded-2xl border border-cyan-300/18 bg-cyan-400/[0.1] px-4 py-3 text-sm font-medium text-cyan-100"
|
|
38
|
+
>
|
|
39
|
+
Probe
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</header>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { workspaceSections, type WorkspaceSectionId } from "../../lib/workspace-sections";
|
|
3
|
+
|
|
4
|
+
export function WorkspaceTabs(props: {
|
|
5
|
+
activeSection: WorkspaceSectionId;
|
|
6
|
+
onSectionChange: (section: WorkspaceSectionId) => void;
|
|
7
|
+
}) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="mt-4 flex items-center gap-2 border-b border-white/6 pb-4">
|
|
10
|
+
{workspaceSections.map((section) => {
|
|
11
|
+
const Icon = section.icon;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<button
|
|
15
|
+
key={section.id}
|
|
16
|
+
type="button"
|
|
17
|
+
onClick={() => props.onSectionChange(section.id)}
|
|
18
|
+
className={clsx(
|
|
19
|
+
"inline-flex items-center gap-2 rounded-2xl border px-4 py-3 text-sm transition",
|
|
20
|
+
props.activeSection === section.id
|
|
21
|
+
? "border-cyan-300/18 bg-cyan-400/[0.1] text-cyan-100"
|
|
22
|
+
: "border-white/8 bg-white/[0.03] text-slate-300 hover:bg-white/[0.05]"
|
|
23
|
+
)}
|
|
24
|
+
>
|
|
25
|
+
<Icon className="h-4 w-4" />
|
|
26
|
+
{section.label}
|
|
27
|
+
</button>
|
|
28
|
+
);
|
|
29
|
+
})}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Activity, Rocket } from "lucide-react";
|
|
2
|
+
import type { PlatformSnapshot, ProbeResult } from "../../types";
|
|
3
|
+
|
|
4
|
+
export function InspectorPanel(props: { platform: PlatformSnapshot; probeResults?: ProbeResult[] }) {
|
|
5
|
+
return (
|
|
6
|
+
<article className="space-y-4">
|
|
7
|
+
<div className="rounded-[24px] border border-white/6 bg-white/[0.025] p-5">
|
|
8
|
+
<div className="text-[11px] uppercase tracking-[0.32em] text-slate-500">Health</div>
|
|
9
|
+
<h2 className="mt-2 font-['Space_Grotesk'] text-[22px] font-bold tracking-[-0.04em] text-slate-50">
|
|
10
|
+
Route latency
|
|
11
|
+
</h2>
|
|
12
|
+
|
|
13
|
+
<div className="mt-4 space-y-2">
|
|
14
|
+
{(props.probeResults || []).map((result) => (
|
|
15
|
+
<div
|
|
16
|
+
key={result.baseUrl}
|
|
17
|
+
className="flex items-center justify-between rounded-2xl border border-white/6 bg-white/[0.03] px-3 py-3"
|
|
18
|
+
>
|
|
19
|
+
<div className="min-w-0 pr-3">
|
|
20
|
+
<div className="truncate text-sm text-slate-200">{result.baseUrl}</div>
|
|
21
|
+
<div className="mt-1 text-[11px] uppercase tracking-[0.24em] text-slate-500">
|
|
22
|
+
{result.ok ? "Available" : "Failed"}
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div className="text-sm font-medium text-cyan-200">{result.latency} ms</div>
|
|
26
|
+
</div>
|
|
27
|
+
))}
|
|
28
|
+
|
|
29
|
+
{!props.probeResults?.length ? (
|
|
30
|
+
<div className="rounded-2xl border border-white/6 bg-white/[0.03] px-3 py-4 text-sm text-slate-500">
|
|
31
|
+
Click Probe in the toolbar to load latency data.
|
|
32
|
+
</div>
|
|
33
|
+
) : null}
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div className="rounded-[24px] border border-white/6 bg-white/[0.025] p-5">
|
|
38
|
+
<div className="text-[11px] uppercase tracking-[0.32em] text-slate-500">Targets</div>
|
|
39
|
+
<h2 className="mt-2 font-['Space_Grotesk'] text-[22px] font-bold tracking-[-0.04em] text-slate-50">
|
|
40
|
+
Live config files
|
|
41
|
+
</h2>
|
|
42
|
+
|
|
43
|
+
<div className="mt-4 space-y-2">
|
|
44
|
+
{props.platform.targetFiles.map((file) => (
|
|
45
|
+
<div key={file} className="rounded-2xl border border-white/6 bg-white/[0.03] px-3 py-3 text-sm text-slate-300">
|
|
46
|
+
{file}
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div className="rounded-[24px] border border-cyan-300/12 bg-gradient-to-br from-cyan-400/[0.08] to-teal-400/[0.05] p-5">
|
|
53
|
+
<div className="flex items-center gap-2 text-sm font-medium text-cyan-100">
|
|
54
|
+
<Rocket className="h-4 w-4" />
|
|
55
|
+
Release Panel
|
|
56
|
+
</div>
|
|
57
|
+
<div className="mt-3 text-sm leading-6 text-slate-300/80">
|
|
58
|
+
Architecture now mirrors the same component-heavy desktop shell direction instead of a single-page mock.
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="rounded-[24px] border border-white/6 bg-white/[0.025] p-5">
|
|
63
|
+
<div className="flex items-center gap-2 text-sm font-medium text-slate-200">
|
|
64
|
+
<Activity className="h-4 w-4 text-emerald-300" />
|
|
65
|
+
Workspace status
|
|
66
|
+
</div>
|
|
67
|
+
<div className="mt-3 space-y-2 text-sm text-slate-400">
|
|
68
|
+
<div className="flex items-center justify-between">
|
|
69
|
+
<span>Provider Sync</span>
|
|
70
|
+
<span className="text-emerald-300">Healthy</span>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="flex items-center justify-between">
|
|
73
|
+
<span>Route Cache</span>
|
|
74
|
+
<span className="text-cyan-200">Ready</span>
|
|
75
|
+
</div>
|
|
76
|
+
<div className="flex items-center justify-between">
|
|
77
|
+
<span>Config Targets</span>
|
|
78
|
+
<span className="text-slate-200">{props.platform.targetFiles.length}</span>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</article>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CloudLightning, FolderOpen, KeyRound, LayoutGrid } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
const icons = [LayoutGrid, KeyRound, CloudLightning, FolderOpen];
|
|
4
|
+
|
|
5
|
+
export function MetricStrip(props: { items: Array<{ label: string; value: string; hint: string }> }) {
|
|
6
|
+
return (
|
|
7
|
+
<section className="mt-4 grid grid-cols-4 gap-3">
|
|
8
|
+
{props.items.map((item, index) => {
|
|
9
|
+
const Icon = icons[index];
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<article key={item.label} className="rounded-[20px] border border-white/6 bg-white/[0.03] px-4 py-4">
|
|
13
|
+
<div className="flex items-center justify-between">
|
|
14
|
+
<div className="text-[11px] uppercase tracking-[0.28em] text-slate-500">{item.label}</div>
|
|
15
|
+
<Icon className="h-4 w-4 text-cyan-300/80" />
|
|
16
|
+
</div>
|
|
17
|
+
<div className="mt-4 font-['Space_Grotesk'] text-2xl font-bold tracking-[-0.04em] text-slate-50">
|
|
18
|
+
{item.value}
|
|
19
|
+
</div>
|
|
20
|
+
<div className="mt-1 text-xs text-slate-500">{item.hint}</div>
|
|
21
|
+
</article>
|
|
22
|
+
);
|
|
23
|
+
})}
|
|
24
|
+
</section>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
type EditorForm = {
|
|
2
|
+
name: string;
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
model: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const modelOptions = ["gpt-5.4", "gpt-5.3-codex", "gpt-5.2", "gpt-5.1-codex-max"];
|
|
9
|
+
|
|
10
|
+
export function ProviderEditor(props: {
|
|
11
|
+
form: EditorForm;
|
|
12
|
+
onChange: (next: EditorForm) => void;
|
|
13
|
+
onSubmit: () => void;
|
|
14
|
+
isSaving: boolean;
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<article className="rounded-[24px] border border-white/6 bg-white/[0.025] p-5">
|
|
18
|
+
<div>
|
|
19
|
+
<div className="text-[11px] uppercase tracking-[0.32em] text-slate-500">Quick Editor</div>
|
|
20
|
+
<h2 className="mt-2 font-['Space_Grotesk'] text-[22px] font-bold tracking-[-0.04em] text-slate-50">
|
|
21
|
+
Provider form
|
|
22
|
+
</h2>
|
|
23
|
+
<p className="mt-2 text-sm leading-6 text-slate-400">
|
|
24
|
+
Compact editor panel, closer to the right-side utility workflow used by CC Switch.
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<form
|
|
29
|
+
className="mt-5 space-y-4"
|
|
30
|
+
onSubmit={(event) => {
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
props.onSubmit();
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<label className="block">
|
|
36
|
+
<div className="mb-2 text-[11px] uppercase tracking-[0.26em] text-slate-500">Provider Name</div>
|
|
37
|
+
<input
|
|
38
|
+
value={props.form.name}
|
|
39
|
+
onChange={(event) => props.onChange({ ...props.form, name: event.target.value })}
|
|
40
|
+
className="w-full rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-3 text-sm text-slate-100 outline-none transition focus:border-cyan-300/24"
|
|
41
|
+
/>
|
|
42
|
+
</label>
|
|
43
|
+
|
|
44
|
+
<label className="block">
|
|
45
|
+
<div className="mb-2 text-[11px] uppercase tracking-[0.26em] text-slate-500">Base URL</div>
|
|
46
|
+
<input
|
|
47
|
+
value={props.form.baseUrl}
|
|
48
|
+
onChange={(event) => props.onChange({ ...props.form, baseUrl: event.target.value })}
|
|
49
|
+
className="w-full rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-3 text-sm text-slate-100 outline-none transition focus:border-cyan-300/24"
|
|
50
|
+
/>
|
|
51
|
+
</label>
|
|
52
|
+
|
|
53
|
+
<label className="block">
|
|
54
|
+
<div className="mb-2 text-[11px] uppercase tracking-[0.26em] text-slate-500">API Key</div>
|
|
55
|
+
<input
|
|
56
|
+
type="password"
|
|
57
|
+
value={props.form.apiKey}
|
|
58
|
+
onChange={(event) => props.onChange({ ...props.form, apiKey: event.target.value })}
|
|
59
|
+
className="w-full rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-3 text-sm text-slate-100 outline-none transition focus:border-cyan-300/24"
|
|
60
|
+
/>
|
|
61
|
+
</label>
|
|
62
|
+
|
|
63
|
+
<label className="block">
|
|
64
|
+
<div className="mb-2 text-[11px] uppercase tracking-[0.26em] text-slate-500">Model</div>
|
|
65
|
+
<select
|
|
66
|
+
value={props.form.model}
|
|
67
|
+
onChange={(event) => props.onChange({ ...props.form, model: event.target.value })}
|
|
68
|
+
className="w-full rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-3 text-sm text-slate-100 outline-none transition focus:border-cyan-300/24"
|
|
69
|
+
>
|
|
70
|
+
{modelOptions.map((item) => (
|
|
71
|
+
<option key={item} value={item} className="bg-slate-950">
|
|
72
|
+
{item}
|
|
73
|
+
</option>
|
|
74
|
+
))}
|
|
75
|
+
</select>
|
|
76
|
+
</label>
|
|
77
|
+
|
|
78
|
+
<button
|
|
79
|
+
type="submit"
|
|
80
|
+
className="w-full rounded-2xl border border-cyan-300/18 bg-cyan-400/[0.12] px-4 py-3 text-sm font-medium text-cyan-100 shadow-[0_12px_28px_rgba(6,182,212,0.12)]"
|
|
81
|
+
>
|
|
82
|
+
{props.isSaving ? "Saving..." : "Save & Apply"}
|
|
83
|
+
</button>
|
|
84
|
+
</form>
|
|
85
|
+
</article>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Plus } from "lucide-react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import type { PlatformSnapshot, Provider } from "../../types";
|
|
4
|
+
|
|
5
|
+
export function ProviderTable(props: {
|
|
6
|
+
platform: PlatformSnapshot;
|
|
7
|
+
onEdit: (provider: Provider) => void;
|
|
8
|
+
onActivate: (providerId: string) => void;
|
|
9
|
+
}) {
|
|
10
|
+
return (
|
|
11
|
+
<article className="rounded-[24px] border border-white/6 bg-white/[0.025] p-5">
|
|
12
|
+
<div className="flex items-center justify-between">
|
|
13
|
+
<div>
|
|
14
|
+
<div className="text-[11px] uppercase tracking-[0.32em] text-slate-500">Provider Matrix</div>
|
|
15
|
+
<h2 className="mt-2 font-['Space_Grotesk'] text-[22px] font-bold tracking-[-0.04em] text-slate-50">
|
|
16
|
+
Saved providers
|
|
17
|
+
</h2>
|
|
18
|
+
<p className="mt-2 text-sm leading-6 text-slate-400">
|
|
19
|
+
The primary working area follows the desktop-table layout instead of stacked marketing cards.
|
|
20
|
+
</p>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<button className="inline-flex items-center gap-2 rounded-xl border border-white/8 bg-white/[0.03] px-3 py-2 text-xs text-slate-300">
|
|
24
|
+
<Plus className="h-4 w-4" />
|
|
25
|
+
Add
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div className="mt-5 overflow-hidden rounded-[20px] border border-white/6 bg-[#0b121a]">
|
|
30
|
+
<div className="grid grid-cols-[1.2fr_0.8fr_1.2fr_0.72fr] gap-3 border-b border-white/6 px-4 py-3 text-[11px] uppercase tracking-[0.24em] text-slate-500">
|
|
31
|
+
<div>Name</div>
|
|
32
|
+
<div>Model</div>
|
|
33
|
+
<div>Base URL</div>
|
|
34
|
+
<div>Status</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div className="divide-y divide-white/6">
|
|
38
|
+
{props.platform.providers.map((provider) => (
|
|
39
|
+
<div
|
|
40
|
+
key={provider.id}
|
|
41
|
+
className={clsx(
|
|
42
|
+
"grid grid-cols-[1.2fr_0.8fr_1.2fr_0.72fr] gap-3 px-4 py-4 transition",
|
|
43
|
+
provider.isActive ? "bg-cyan-400/[0.06]" : "hover:bg-white/[0.03]"
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
<div className="min-w-0">
|
|
47
|
+
<div className="truncate text-sm font-medium text-slate-100">{provider.name}</div>
|
|
48
|
+
<div className="mt-1 text-xs text-slate-500">{provider.updatedAt.slice(0, 10)}</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="min-w-0">
|
|
51
|
+
<div className="truncate text-sm text-slate-300">{provider.model}</div>
|
|
52
|
+
<div className="mt-1 text-xs text-slate-500">{provider.maskedApiKey}</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="min-w-0">
|
|
55
|
+
<div className="truncate text-sm text-slate-300">{provider.baseUrl}</div>
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={() => props.onEdit(provider)}
|
|
59
|
+
className="mt-2 rounded-lg border border-white/8 bg-white/[0.03] px-2.5 py-1 text-[11px] text-slate-300"
|
|
60
|
+
>
|
|
61
|
+
Edit
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
<div className="flex flex-col items-start gap-2">
|
|
65
|
+
{provider.isActive ? (
|
|
66
|
+
<span className="rounded-full bg-cyan-400/12 px-2.5 py-1 text-[11px] font-medium text-cyan-200">
|
|
67
|
+
Active
|
|
68
|
+
</span>
|
|
69
|
+
) : (
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
onClick={() => props.onActivate(provider.id)}
|
|
73
|
+
className="rounded-lg border border-cyan-300/14 bg-cyan-400/[0.08] px-3 py-1.5 text-[11px] text-cyan-100"
|
|
74
|
+
>
|
|
75
|
+
Enable
|
|
76
|
+
</button>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</article>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function LogoMark() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="h-11 w-11 drop-shadow-[0_16px_30px_rgba(12,142,127,0.24)]">
|
|
4
|
+
<svg viewBox="0 0 128 128" className="h-11 w-11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
5
|
+
<defs>
|
|
6
|
+
<linearGradient id="fml-bg-react" x1="16" y1="12" x2="108" y2="116" gradientUnits="userSpaceOnUse">
|
|
7
|
+
<stop stopColor="#071A24" />
|
|
8
|
+
<stop offset="0.55" stopColor="#103847" />
|
|
9
|
+
<stop offset="1" stopColor="#0C8E7F" />
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<radialGradient id="fml-glow-react" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(96 28) rotate(135) scale(42)">
|
|
12
|
+
<stop stopColor="#5EEAD4" stopOpacity="0.85" />
|
|
13
|
+
<stop offset="1" stopColor="#5EEAD4" stopOpacity="0" />
|
|
14
|
+
</radialGradient>
|
|
15
|
+
<linearGradient id="fml-stroke-react" x1="24" y1="30" x2="98" y2="98" gradientUnits="userSpaceOnUse">
|
|
16
|
+
<stop stopColor="#FFF9EE" />
|
|
17
|
+
<stop offset="1" stopColor="#D5FBF5" />
|
|
18
|
+
</linearGradient>
|
|
19
|
+
</defs>
|
|
20
|
+
<rect x="8" y="8" width="112" height="112" rx="30" fill="url(#fml-bg-react)" />
|
|
21
|
+
<rect x="8.75" y="8.75" width="110.5" height="110.5" rx="29.25" stroke="rgba(255,255,255,0.12)" strokeWidth="1.5" />
|
|
22
|
+
<circle cx="96" cy="28" r="30" fill="url(#fml-glow-react)" opacity="0.72" />
|
|
23
|
+
<path d="M26 36V92" stroke="url(#fml-stroke-react)" strokeWidth="10" strokeLinecap="round" />
|
|
24
|
+
<path d="M26 36H54" stroke="url(#fml-stroke-react)" strokeWidth="10" strokeLinecap="round" />
|
|
25
|
+
<path d="M26 62H47" stroke="url(#fml-stroke-react)" strokeWidth="10" strokeLinecap="round" />
|
|
26
|
+
<path d="M60 92V36L74 62L88 36V92" stroke="url(#fml-stroke-react)" strokeWidth="10" strokeLinecap="round" strokeLinejoin="round" />
|
|
27
|
+
<path d="M88 92H102" stroke="#F3C46C" strokeWidth="10" strokeLinecap="round" />
|
|
28
|
+
<path d="M102 36V92" stroke="#F3C46C" strokeWidth="10" strokeLinecap="round" />
|
|
29
|
+
</svg>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const connectors = [
|
|
2
|
+
{ name: "Filesystem", status: "Connected", detail: "Workspace root mounted with read/write scope" },
|
|
3
|
+
{ name: "Browser", status: "Ready", detail: "Web browsing enabled for docs, releases and references" },
|
|
4
|
+
{ name: "Git", status: "Healthy", detail: "Repository metadata and branch graph available" },
|
|
5
|
+
{ name: "Shell", status: "Allowed", detail: "Local command execution within workspace boundaries" }
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
export function McpView() {
|
|
9
|
+
return (
|
|
10
|
+
<section className="mt-4 grid min-h-0 flex-1 grid-cols-[1.1fr_0.9fr] gap-4">
|
|
11
|
+
<article className="rounded-[24px] border border-white/6 bg-white/[0.025] p-5">
|
|
12
|
+
<div className="text-[11px] uppercase tracking-[0.32em] text-slate-500">Connector Registry</div>
|
|
13
|
+
<h2 className="mt-2 font-['Space_Grotesk'] text-[22px] font-bold tracking-[-0.04em] text-slate-50">
|
|
14
|
+
MCP endpoints
|
|
15
|
+
</h2>
|
|
16
|
+
<div className="mt-5 space-y-3">
|
|
17
|
+
{connectors.map((item) => (
|
|
18
|
+
<div key={item.name} className="rounded-[20px] border border-white/6 bg-white/[0.03] p-4">
|
|
19
|
+
<div className="flex items-center justify-between">
|
|
20
|
+
<div className="text-sm font-medium text-slate-100">{item.name}</div>
|
|
21
|
+
<div className="rounded-full bg-cyan-400/12 px-2.5 py-1 text-[11px] text-cyan-200">{item.status}</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div className="mt-2 text-sm leading-6 text-slate-400">{item.detail}</div>
|
|
24
|
+
</div>
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
</article>
|
|
28
|
+
|
|
29
|
+
<article className="rounded-[24px] border border-white/6 bg-white/[0.025] p-5">
|
|
30
|
+
<div className="text-[11px] uppercase tracking-[0.32em] text-slate-500">Policy Surface</div>
|
|
31
|
+
<h2 className="mt-2 font-['Space_Grotesk'] text-[22px] font-bold tracking-[-0.04em] text-slate-50">
|
|
32
|
+
Access profile
|
|
33
|
+
</h2>
|
|
34
|
+
<div className="mt-5 space-y-3">
|
|
35
|
+
{["Workspace Write", "Network Access", "Prompt Upgrade", "Parallel Agents"].map((item) => (
|
|
36
|
+
<div key={item} className="flex items-center justify-between rounded-[18px] border border-white/6 bg-white/[0.03] px-4 py-3">
|
|
37
|
+
<span className="text-sm text-slate-200">{item}</span>
|
|
38
|
+
<span className="text-xs uppercase tracking-[0.24em] text-emerald-300">Enabled</span>
|
|
39
|
+
</div>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
</article>
|
|
43
|
+
</section>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const promptCards = [
|
|
2
|
+
{ name: "Default Coding", tone: "Direct", notes: "For implementation, debugging and repo-level changes." },
|
|
3
|
+
{ name: "UI Review", tone: "Critical", notes: "For visual comparison, layout review and polish passes." },
|
|
4
|
+
{ name: "Provider Setup", tone: "Operational", notes: "For relay config, route switching and install flows." }
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
export function PromptsView() {
|
|
8
|
+
return (
|
|
9
|
+
<section className="mt-4 grid min-h-0 flex-1 grid-cols-[1.18fr_0.82fr] gap-4">
|
|
10
|
+
<article className="rounded-[24px] border border-white/6 bg-white/[0.025] p-5">
|
|
11
|
+
<div className="text-[11px] uppercase tracking-[0.32em] text-slate-500">Prompt Library</div>
|
|
12
|
+
<h2 className="mt-2 font-['Space_Grotesk'] text-[22px] font-bold tracking-[-0.04em] text-slate-50">
|
|
13
|
+
Saved prompt profiles
|
|
14
|
+
</h2>
|
|
15
|
+
<div className="mt-5 space-y-3">
|
|
16
|
+
{promptCards.map((item) => (
|
|
17
|
+
<div key={item.name} className="rounded-[20px] border border-white/6 bg-white/[0.03] p-4">
|
|
18
|
+
<div className="flex items-center justify-between">
|
|
19
|
+
<div className="text-sm font-medium text-slate-100">{item.name}</div>
|
|
20
|
+
<div className="rounded-full bg-white/[0.05] px-2.5 py-1 text-[11px] text-slate-300">{item.tone}</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div className="mt-2 text-sm leading-6 text-slate-400">{item.notes}</div>
|
|
23
|
+
</div>
|
|
24
|
+
))}
|
|
25
|
+
</div>
|
|
26
|
+
</article>
|
|
27
|
+
|
|
28
|
+
<article className="rounded-[24px] border border-white/6 bg-white/[0.025] p-5">
|
|
29
|
+
<div className="text-[11px] uppercase tracking-[0.32em] text-slate-500">Active Prompt</div>
|
|
30
|
+
<h2 className="mt-2 font-['Space_Grotesk'] text-[22px] font-bold tracking-[-0.04em] text-slate-50">
|
|
31
|
+
Prompt editor
|
|
32
|
+
</h2>
|
|
33
|
+
<div className="mt-5 rounded-[20px] border border-white/6 bg-[#0b121a] p-4 text-sm leading-7 text-slate-300">
|
|
34
|
+
You are a coding assistant for ccon. Prioritize provider workflows, desktop release preparation,
|
|
35
|
+
route health, and concrete implementation over abstract planning.
|
|
36
|
+
</div>
|
|
37
|
+
</article>
|
|
38
|
+
</section>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { InspectorPanel } from "../../components/providers/inspector-panel";
|
|
2
|
+
import { MetricStrip } from "../../components/providers/metric-strip";
|
|
3
|
+
import { ProviderEditor } from "../../components/providers/provider-editor";
|
|
4
|
+
import { ProviderTable } from "../../components/providers/provider-table";
|
|
5
|
+
import type { ReturnTypeUseControlWorkspace } from "./types";
|
|
6
|
+
|
|
7
|
+
export function ProvidersView(props: { workspace: ReturnTypeUseControlWorkspace }) {
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<MetricStrip items={props.workspace.metrics} />
|
|
11
|
+
|
|
12
|
+
<section className="mt-4 grid min-h-0 flex-1 grid-cols-[1.2fr_0.94fr_0.72fr] gap-4">
|
|
13
|
+
<ProviderTable
|
|
14
|
+
platform={props.workspace.platform}
|
|
15
|
+
onEdit={props.workspace.openProvider}
|
|
16
|
+
onActivate={(providerId) =>
|
|
17
|
+
props.workspace.activateMutation.mutate({
|
|
18
|
+
platform: props.workspace.platform.id,
|
|
19
|
+
providerId
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
/>
|
|
23
|
+
|
|
24
|
+
<ProviderEditor
|
|
25
|
+
form={props.workspace.editorForm}
|
|
26
|
+
onChange={props.workspace.setEditorForm}
|
|
27
|
+
onSubmit={() =>
|
|
28
|
+
props.workspace.saveMutation.mutate({
|
|
29
|
+
platform: props.workspace.platform.id,
|
|
30
|
+
...props.workspace.editorForm
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
isSaving={props.workspace.saveMutation.isPending}
|
|
34
|
+
/>
|
|
35
|
+
|
|
36
|
+
<InspectorPanel platform={props.workspace.platform} probeResults={props.workspace.probeQuery.data?.results} />
|
|
37
|
+
</section>
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PlatformId, PlatformSnapshot, ProbeResult, Provider } from "../../types";
|
|
2
|
+
|
|
3
|
+
export type ProviderEditorForm = {
|
|
4
|
+
name: string;
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
apiKey: string;
|
|
7
|
+
model: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ReturnTypeUseControlWorkspace = {
|
|
11
|
+
metrics: Array<{ label: string; value: string; hint: string }>;
|
|
12
|
+
platform: PlatformSnapshot;
|
|
13
|
+
openProvider: (provider: Provider) => void;
|
|
14
|
+
activateMutation: {
|
|
15
|
+
mutate: (input: { platform: PlatformId; providerId: string }) => void;
|
|
16
|
+
};
|
|
17
|
+
editorForm: ProviderEditorForm;
|
|
18
|
+
setEditorForm: (next: ProviderEditorForm) => void;
|
|
19
|
+
saveMutation: {
|
|
20
|
+
mutate: (input: { platform: PlatformId; name: string; baseUrl: string; apiKey: string; model: string }) => void;
|
|
21
|
+
isPending: boolean;
|
|
22
|
+
};
|
|
23
|
+
probeQuery: {
|
|
24
|
+
data?: { results: ProbeResult[] };
|
|
25
|
+
};
|
|
26
|
+
};
|