Four Levels Of Customer Understanding

Many companies think they know fairly well what their users want and need, and how they make their decisions. Yet most of the time these are merely big assumptions and big hunches — with little real evidence to support them. In practice, obvious reasons might be true, but they rarely paint the full picture.

To understand our customers, we must triangulate across four levels of customer understanding by Hannah Shamji. It’s a useful way to think about the underlying reasons for user behavior, hidden motivations, and the complex layers of messy and noisy reality that are often overlooked. Let’s see how it works.

Don’t Ask Users Your Burning Questions

To learn about customers, it might seem reasonable to ask people what they think and draw conclusions from it. But it’s rarely an effective way to get actionable answers. In fact, as it turns out, what people think, feel, say, and _do_ are often very different things.

As Erika Hall wrote, asking a question directly is the worst way to get a true and useful answer to that question. We don’t always understand or are aware of our true motivations. We often apply our own context and interpretations to questions.

We also exaggerate (a lot!). We focus on edge cases and unrealistic scenarios, and we favor short-term goals over long-term goals. So if users say that they absolutely need to compare products in a table, it doesn’t mean that they couldn’t get to their underlying goal without it.

“Possible” vs. “Probable”

Just to indicate how tricky listening to words alone is: even little nuances in words chosen matter. In practice, users are rarely precise in expressing their thoughts, and a good example is the distinction between possible, plausible, and probable, as discovered by Thomas D’hooge.

A study on Dutch verbal probability terms shows how unreliable the choice of words is. While extreme words have some agreement, terms like “possible,” “maybe,” “uncertain,” or “likely” lead to a wide spread of interpretations. So we shouldn’t rely on what people say, but rather try to go deeper.

The Levels Of Understanding

To get a more realistic and less biased view of customers’ needs, we need to understand a broader picture across 4 levels:

  • Level 1: “What they say”
    Easier to collect, but mostly opinions, and most unreliable. People often explain their behavior through the lens of how they perceive it, or how they want it to be perceived, which isn’t always accurate. We shouldn’t rely too much on CRM data, surveys, or polls.
  • Level 2: “What they think and feel”
    Gives more context, but is still heavily shaped by memory and personal preferences. Good user research and interviews help us understand expectations and experiences.
  • Level 3: “What they do”
    We study actual behavior, actions taken or skipped, usage data, and analytics. We run task analysis and workflow analysis to understand how people use the product.
  • Level 4: “Why they do it”
    We study underlying motivations and root causes, through observations of real workflows and in-depth interviews. Typically, it requires a trustworthy relationship with the user, repeat interviews, and task walkthroughs.

Personally, I wouldn’t recommend NPS (alternative). It’s worth noting that different levels might reveal conflicting or contradictory data. To get a better understanding, we need to triangulate and reconcile data with mixed-method research.

Capturing Emotions And Nuance

Emotions are always difficult to capture, but they are easier to spot once you observe people doing what they need to do without external influence or interruptions. The ability to positively impact users grows by moving from sympathy to empathy or even compassion, as articulated by Sarah Gibbons.

In the past, I was using “speak-aloud” protocol and asked users to walk me through their thought process as they were completing tasks. But it actually turns out to be quite disruptive. Because people are focused on speaking at the same time while solving a task, many emotions remain hidden or obscured by their language.

So, when conducting usability testing, I don’t ask users to speak through their experience. Instead, I observe where they tap or hover with the mouse, where their mouse circles without an action, where they scroll, and how long. Eventually, when a user confirms that they are done or that they are stuck, I ask questions.

The Emotion Wheel (website) by Geoffrey Roberts is a helpful little tool for better describing a range of emotions during user interviews or design sessions. It certainly needs refinement for product design needs, but it helps us get more precise about the sentiment customers or colleagues might be experiencing, moving beyond just “good” or “bad”.

One helpful trick is to use mirroring — repeating what a user has said, or ask the same question twice, just paraphrasing it. Or navigating the emotions wheel (see above) to better capture and understand the emotion.

These strategies help uncover some of the issues that perhaps didn’t come up in the first answer. That’s also when a user tends to add more useful context and details as they explain their confusion.

Emotions Aren’t Everything

Some people strongly disagree:

“Our work is about others — their problems, their pain, their mess. Our job is to make sense of it and then do something about it. Not to emote or perform but to act on and solve it. There is a flawed belief that to build great things, you first need to emotionally fully absorb someone else’s experience.”

— Alin Buda

I think that Alin brings up a very strong argument, and personally, I find it difficult to disagree with. However, I do see user’s emotional response as a signal of how well the product is working for them. How engaged or detached they are in their journey, how they react to aesthetics, how confused or confident they are.

Ultimately, these are signals. To make a difference, we must go beyond emotions and explore what people actually do. Usually, this means relentlessly observing, diagnosing, and focusing on underlying user needs.

Observe And Diagnose, Don’t Validate

Instead of asking, we need to observe. Usually, I focus on small things that make or break an experience. I see where users lose time, repeat actions, hover without clicking, or click and then go back. Pay attention to subtle cues like scratching their neck, raising eyebrows, or expressions of worry, joy, or confusion.

Many companies talk about “validation” through user testing, but often that means simply confirming existing assumptions. But we should instead diagnose existing behavior without preconceived notions or affiliations. We don’t validate — we actually research instead.

That research means not just understanding customers’ real motivations, but also risks, doubts, concerns, worries, and perhaps even harms.

The only way to get there is by building a sincere, honest, and trustworthy relationship — one that feels right and resonates deeply. When customers truly care and want to help, getting to a real understanding becomes much, much easier.

Practical Ways To Uncover User Needs

We don’t need expensive tools to uncover user needs. David Travis provides a fantastic overview of helpful strategies to do just that. Here are some initiatives to spread the word about real user’s struggles or gain a deeper understanding of user needs:

  • Exposure hours, when every employee must be exposed to their customers for at least 2 hours every 6–12 weeks.
  • Live UX testing, where we invite everyone in the company to join and observe.
  • Co-design with users, where we show new features and ask users to rank them.
  • Helpdesk insights, where we ask for frequent complaints and questions from the support every 3–6 months.
  • Listening in, where we tune in on a customer service call, web chat, or eavesdrop where users hang out.

The core idea here is that you don’t need extensive and expensive tools to uncover user needs. You need to create spaces where customers’ struggles can be exposed and make these struggles visible across the entire company.

It can be short video clips of user sessions or a monthly newsletter with what we learned this month. Making these pain points visible can rally everyone from marketing to engineering to keep users’ struggles at the back of their minds.

Wrapping Up

To make an impact, we must go way beyond user feedback. It’s never enough to listen to surveys — we must observe customers’ actual behaviors and build relationships to truly understand their goals and their motivations.

And most importantly, we need to understand what questions we actually want to have answered. Not what “validation” we need to move on with the project, but what we don’t know and what we need to research.

Without it, everything else is merely hunches and assumptions — and often wrong and expensive ones.

Meet “Measure UX & Design Impact”

Meet Measure UX & Design Impact, Vitaly’s practical guide for designers and UX leads on how to track and visualize the incredible impact of your UX work on business — with a live UX training later this year. Jump to details.


Meet Measure UX and Design Impact, a practical video course for designers and UX leads.

  • Video + UX Training
  • Video only

Video + UX Training

$ 495.00 $ 799.00

Get Video + UX Training

25 video lessons (8h) + Live UX Training.
100 days money-back-guarantee.

Video only

$ 250.00$ 350.00

Get the video course

25 video lessons (8h). Updated yearly.
Also available as a UX Bundle with 3 video courses.

Useful Resources

  • Four Levels of Customer Understanding, by Hannah Shamji
  • 60 Ways To Understand User Needs, by David Travis
  • Emotion Wheel Toolkit (PNG), by Geoffrey Roberts
  • Feelings Wheel PDF
  • Feelings Wheel Online
  • My Case Against Empathy, by Alin Buda
  • Possible vs. Probable, by Thomas D’hooge
  • Communicating probability: a multinational study of the interpretation of verbal probability terms, by Maarten C. de Vries, Marjolijn L. de Boer, and Martine Bouman.

Useful Books

  • Deploy Empathy: A practical guide to interviewing customers, by Michele Hansen
  • Humankind, by Rutger Bregman

What If AI Didn’t Need the Internet?

*How Gemma 4 Could Bring Powerful AI Closer to People, Not Just Servers

For a long time, using powerful AI has felt a bit like borrowing someone else’s computer from far away. Every question, image, or request had to travel through the internet to massive cloud servers before coming back with an answer. It worked, but it also came with limits — slow connections, privacy concerns, expensive APIs, and the constant need to stay online.

But recently, that idea started to change for me.

When I explored Google’s Gemma 4 models, I realized something important: the future of AI may not live only in giant data centers. It may live on our own devices.

And honestly, that feels like a breath of fresh air.

Why Local AI Feels Different

Most conversations around AI focus on bigger models, faster responses, or smarter chatbots. But what excited me most about Gemma 4 was something simpler — accessibility.

Gemma 4 introduces powerful capabilities like multimodal understanding, advanced reasoning, and a huge 128K context window, while also supporting local deployments across different kinds of devices. That means AI is no longer only for companies with huge budgets and powerful servers. In many ways, it levels the playing field.

For students, creators, and independent developers, that matters a lot.

Sometimes the best ideas come from people who do not have unlimited resources. Gemma 4 feels like a tool that opens doors instead of building walls.

A Future Beyond Constant Connectivity

In many places, stable internet is still not guaranteed. Even today, students often struggle with weak networks, limited data plans, or shared devices. Cloud-based AI can become difficult to rely on in those situations.

Now imagine this instead:

A student sitting in a small town using an offline AI tutor on a low-cost laptop.

A medical worker accessing a private assistant without uploading sensitive information to external servers.

A creator brainstorming ideas while traveling without worrying about internet speed.

That is where local AI starts to shine.

As the saying goes, “necessity is the mother of invention.” Sometimes limitations push technology in the right direction. Local AI is not just about convenience — it is about making intelligence more available to ordinary people.

What Makes Gemma 4 Exciting

One thing I appreciate about Gemma 4 is that it does not feel like a one-size-fits-all solution. The different model sizes make it flexible enough for different needs, from lightweight experiments to larger applications.

The multimodal capability is especially interesting because it allows AI to work with more than just text. That opens the door for tools that can understand images, documents, notes, and visual information in a much more natural way.

The long context window also caught my attention. Anyone who has worked with AI knows how frustrating it can be when a model “forgets” earlier parts of a conversation or document. With 128K context support, Gemma 4 can handle much larger amounts of information at once, making interactions feel smoother and more useful.

And then there is reasoning.

We are slowly moving from AI that simply responds to AI that can genuinely assist with problem-solving and deeper thinking tasks. That shift could change how students learn, how developers build, and how small teams innovate.

Why This Matters to Me as a Student Developer

As a student developer, what inspires me most is not just the technology itself — it is the possibility behind it.

Powerful AI often feels out of reach for many learners. Either the hardware is expensive, the APIs cost too much, or the tools require constant internet access. It can feel like the cards are stacked against small creators.

But local AI changes the conversation.

It gives students the freedom to experiment, build, and learn without depending entirely on cloud platforms. Even simple projects can become meaningful. An offline study assistant, a note summarizer, a multilingual learning tool — these ideas suddenly feel possible.

That is exciting because innovation should not belong only to large companies. Sometimes a small idea in the right hands can punch above its weight.

The Bigger Picture

I do not think the future will be “cloud AI versus local AI.” Both will continue to exist and grow together.

But I do believe local AI will become increasingly important.

People care more about privacy now. Developers want flexibility. Students want affordable tools. And many communities still need technology that works even with limited connectivity.

Gemma 4 feels like a step toward that future — one where AI becomes more personal, more accessible, and more adaptable to real-world situations.

Not every technological shift changes who gets to participate.

This one just might.

Final Thoughts

The most impressive thing about Gemma 4 is not only its capabilities. It is the idea behind it.

Powerful AI is slowly moving closer to people instead of farther away behind massive infrastructure. And that could make all the difference.

We often say technology should make life easier, but the best technology also makes opportunities wider. In many ways, Gemma 4 feels like a glimpse of that future.

And for students, builders, and curious minds everywhere, that future looks incredibly promising.

Terraform with AI: Build AWS Infra (Cursor + MCP)

Why Terraform with AI Matters in Modern DevOps

Writing Terraform for anything beyond a small setup quickly becomes tedious.
Once you start dealing with multiple modules, cross-resource dependencies, and AWS-specific quirks, the workflow slows down. Most of the time isn’t spent writing code — it’s spent checking documentation, fixing edge cases, and rerunning terraform apply.
Many teams are now experimenting with Terraform with AI to speed this up.
In practice, that only works partially — unless the AI has proper context.
Build Complete AWS Infrastructure with Terraform MCP Server and Cursor AI – Full Tutorial

How Terraform workflows traditionally worked

A typical workflow looks like this:
Read Terraform docs
Write modules and resources manually
Run terraform plan
Fix errors
Repeat
For small setups, this is manageable.
For production infrastructure, it becomes repetitive and slow. Most engineers end up switching between Terraform registry docs, AWS docs, and their codebase constantly.

Limitations of Using Terraform with AI Without Context

The obvious idea is to use AI to generate Terraform.
In most cases, it starts like this:

“Generate Terraform for a VPC with public and private subnets”
You do get output. But:

It may use outdated arguments
It ignores your module structure
Dependencies are incomplete
It often fails during terraform apply
👉 The core issue: AI does not understand your infrastructure context

Our First Attempt (RAG Failure) (Late 2024 – before advent of modern agents)

To solve this, we built an internal tool using:

Vector database
RAG (Retrieval-Augmented Generation)
The idea was to fetch Terraform documentation and index it in a vector database and provide it to an agent
It helped slightly — but failed in practice:

Iteration was difficult – terraform plan and apply loop – fix errors
Context size limitations
No awareness of project structure
Could not refine outputs
It generated code, but only for simple infrastructure. For complex ones it used to fail after a few iterations.

We didn’t try to optimise it further because while we were in middle of it – cursor agents became extremely powerful and they pretty much solved this iteration problem.

What changed with MCP Server + Cursor

The behavior changed once we introduced Terraform MCP Server and used it with Cursor.
Instead of generating code blindly, the system now had access to:

Terraform module documentation
Input/output structures
Resource relationships
The difference was noticeable.
The output was not perfect — but much closer to something usable.

How MCP actually changes the workflow

At a high level, MCP acts as a bridge between the editor (Cursor) and Terraform context.
Instead of guessing, the AI can:

Look up module definitions
Understand required inputs
Follow dependencies across resources

This is the key difference from standard AI usage.

⚙️ What MCP Server Actually Does Internally

The improvement with MCP is not just better prompting — it’s access to structured Terraform knowledge.
The MCP server exposes tools that allow the AI to query real Terraform data:

Key MCP Capabilities:

Provider Documentation Lookup

Fetches full documentation for resources, data sources, and functions

Module Discovery

Finds Terraform modules from the registry with usage examples

Module Details

Retrieves inputs, outputs, and configuration patterns

Policy Search

Helps identify best practices and security policies
👉 In simple terms:
Instead of guessing, the AI can look things up like an engineer would.

Terraform with AI vs Manual vs MCP (Comparison)

In practice, most teams try AI first, then realize that without context, results are unreliable. MCP fixes that gap.

A practical example: building AWS infrastructure

Let’s take a realistic setup:

VPC with public and private subnets
NAT Gateway and Internet Gateway
Application Load Balancer
Auto Scaling group (EC2)
CloudFront distribution
Cloudflare DNS
Jump box for access
This is a typical production-style setup.
Writing this manually takes time — especially when wiring dependencies correctly.

How we approached it

Instead of writing everything manually, we broke the problem into smaller steps and guided the AI.

🧠 Full Prompt Used for Infrastructure Generation

Instead of vague prompts, we used a structured, step-by-step approach to guide the AI.

Start code generation. Do it step by step. Move to next step only after the current step is complete.

Step: Create VPC and Network Infrastructure - use vpc module
- Create VPC with appropriate CIDR block
- Create public and private subnets across 2 AZs
- Set up Internet Gateway
- Configure NAT Gateways in public subnets
- Configure route tables for public/private subnets

Step: Create Security Groups
- ALB security group (allow HTTP/HTTPS inbound)
- EC2 security group 
    - allow traffic from ALB
    - allow ssh from the vpc
- Allow all outbound traffic

Step: Create Auto Scaling Group - use autoscaling module
- Create launch template for EC2 instances
- Use ubuntu ami for the instances
- Configure ASG across private subnets
- Use keypair named "vikas-aws"
- Add a user data script to install nginx and create a simple html page

Step: Create a jumpbox
- Create a jumpbox in the public subnet
- Ensure it has a public IP
- Allow SSH from internet

Step: Create Application Load Balancer - use alb module
- Create ALB in public subnets
- Configure HTTP listener
- Attach to autoscaling group

Step: CloudFront Distribution
- Configure CloudFront with ALB as origin
- Set caching TTL to 0

Step: DNS Configuration
- DNS handled via Cloudflare (no route53)

In practice, breaking the problem into steps like this improves output quality significantly compared to single prompts.

What the generated Terraform looked like

A simplified example:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"

  name = "demo-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b"]
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.3.0/24", "10.0.4.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = true
}

This wasn’t perfect out of the box, but:

Structure was correct
Inputs were mostly valid
Dependencies were aligned
That already saves a significant amount of time.

What actually improved (based on usage)

From real usage:

Before:

2–4 hours to assemble infra
Multiple documentation lookups
Several failed applies

After:

Initial setup generated in minutes
Fewer structural errors
Faster iteration
In most teams, the biggest gain is reduced context switching.

Operational considerations

This approach still requires discipline:

Always run terraform plan
Review changes carefully
Do not trust generated code blindly
IAM policies and security configurations must always be reviewed manually.

When Terraform with AI Works Best

In most teams, this approach works well when:

You are building new infrastructure
You need to scaffold modules quickly
You want to reduce repetitive work
It is less effective when used blindly or without validation.

When not to use this approach

Avoid relying on it when:

Infrastructure requires strict compliance
You don’t understand the generated code
You need deterministic, audited configurations
This is not a replacement for Terraform expertise.

Where this fits in a DevOps workflow

This approach integrates naturally with:

Git-based workflows
CI/CD pipelines
Infrastructure reviews
The deployment process does not change — only the way code is written.

FAQ

What is Terraform with AI?

Terraform with AI refers to using AI tools to generate and manage infrastructure code more efficiently.

What is Terraform MCP Server?

It provides AI tools with Terraform context, including modules and documentation.

Is AI-generated Terraform safe for production?

Yes, but only after proper validation and review.

Conclusion

Terraform itself hasn’t changed.
What’s changing is how engineers interact with it.
Using Terraform with AI + MCP Server reduces friction in writing infrastructure — especially for repetitive setups.
It doesn’t replace engineering judgment, but it does make the workflow more efficient.

Related reading

https://www.kubeblogs.com/how-civo-kubernetes-routes-pod-traffic-single-egress-ip-explained/
https://www.kubeblogs.com/gp3-vs-gp2-ebs-volume-aws/

CSS :has() Selector: The Layout Trick I Wish I Knew 5 Years Ago

CSS :has() is not just a fancy :parent

When :has() started popping up in specs and tweets, I mentally filed it under “cool, but not for shipping work.” I was wrong.

Now it is in Chrome, Safari, Edge, and Firefox. I use it in real projects. It has removed entire JavaScript files and a pile of .is-active classes that I was embarrassed to maintain.

If you are a working frontend dev, the shorthand is this: :has() turns CSS from “style what is there” into “style this thing if it contains that thing”. That one capability changes layout, state, and validation flows.

I will walk through three places where it made a real difference for me:

  • Parent styling without JS
  • Sibling state UIs without wiring events
  • Form validation UI that reacts to the DOM, not a framework

All of this shipped with zero additional JavaScript.

Quick mental model of :has()

The syntax looks like a pseudo class on a selector:

.card:has(img.hero) {
  /* styles here */
}

Read it as: “select .card elements that have a descendant img.hero somewhere inside.” It is a conditional filter on the left side of the selector.

You can also scope it more tightly:

.tabs:has(> .tab.is-active) {
  /* direct children only */
}

Or use it with relational selectors like siblings:

.field:has(+ .field--error) {
  /* this .field is followed by an error field */
}

Once that clicks, you start seeing places to remove JS.

1. Parent styling in a content-heavy project

First real use: a content-heavy marketing site for a biohacking brand I work with. Editors can drop components in any order with a CMS. Sometimes a card has an image, sometimes it is text-only. The layout should adapt.

Previously I solved this with modifier classes from the CMS, or a hydration script that scans the DOM and adds classes like .card--with-media. Boring, fragile, and slightly gross.

With :has() I deleted that script.

Image-aware cards

The card markup is boring on purpose:

<article class="card">
  <img class="card__media" src="hero.jpg" alt="">
  <div class="card__body">
    <h2>Title</h2>
    <p>Some text...</p>
  </div>
</article>

<article class="card">
  <div class="card__body">
    <h2>Another Card</h2>
    <p>Text-only card.</p>
  </div>
</article>

Now the CSS decides layout based on presence of media.

.card {
  display: grid;
  gap: 1rem;
}

.card:has(.card__media) {
  grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
  align-items: center;
}

.card:not(:has(.card__media)) {
  padding: 2rem;
  background: #111;
  color: #eee;
}

Result: if marketing drops in an image, the card becomes a two-column layout. If not, it becomes a full-width text block. No new class. No CMS configuration. No JS.

I like this because the markup stays semantic and dumb. The layout is a true function of the content, which is what CSS was always supposed to do but rarely could at the parent level.

Auto-promoting “hero” sections

Same project. Editors could add a .section stack: some had a prominent CTA, some were just copy. If a section had a primary CTA, design wanted extra padding and a gradient background.

<section class="section">
  <h2>Get early access</h2>
  <p>Short description.</p>
  <a class="btn btn--primary" href="#">Join the beta</a>
</section>

<section class="section">
  <h2>What you get</h2>
  <p>More text...</p>
</section>

With :has() I treat any section with a primary button as a pseudo hero.

.section {
  padding: 2rem 1.5rem;
  background: #050505;
}

.section:has(.btn--primary) {
  padding: 4rem 1.5rem;
  background: radial-gradient(circle at top, #2f80ed, #050505);
  color: #fff;
}

.section:has(.btn--primary) h2 {
  font-size: 2.25rem;
}

That tiny selector replaced a custom “hero” block type in the CMS that content editors kept misusing. I stopped explaining “use the hero component for this” and just let the CSS infer intent from presence of a primary CTA.

You can do similar things with :has(video), :has(.badge--new), etc. It is a good fit for messy CMS content where you want layout to respond to what your editors actually do, not what the schema designer hoped they would do.

2. Sibling state UIs without event listeners

Second use case: stateful UIs that I used to wire up with click handlers. Tabs, disclosure panels, navigation highlights, that stuff.

Yes, you can still do it in JS. But if the state is already visible in the DOM, :has() lets CSS own more of the behavior. That means less code, fewer states to sync, and fewer bugs.

Tabs powered by :target and :has()

On a little side project for baseball drills, I built a tabbed interface where each tab is actually a link to an anchor. I wanted a sticky tab bar that changes style when any tab content is active.

<div class="tabs">
  <nav class="tabs__nav">
    <a href="#hitting">Hitting</a>
    <a href="#pitching">Pitching</a>
    <a href="#fielding">Fielding</a>
  </nav>

  <section id="hitting" class="tabs__panel">...</section>
  <section id="pitching" class="tabs__panel">...</section>
  <section id="fielding" class="tabs__panel">...</section>
</div>

The panels show / hide with a regular :target trick.

.tabs__panel {
  display: none;
}

.tabs__panel:target {
  display: block;
}

Old me would now add JS to toggle classes on the nav. Instead I lean on :has().

.tabs {
  border-bottom: 1px solid #333;
}

.tabs__nav a {
  padding: .5rem 1rem;
  text-decoration: none;
  color: #888;
}

.tabs__nav a:is(:hover, :focus-visible) {
  color: #fff;
}

/* highlight the active tab label */
.tabs__nav a[href^="#"] {
  position: relative;
}

.tabs:has(#hitting:target) .tabs__nav a[href="#hitting"],
.tabs:has(#pitching:target) .tabs__nav a[href="#pitching"],
.tabs:has(#fielding:target) .tabs__nav a[href="#fielding"] {
  color: #fff;
  font-weight: 600;
}

/* make the whole tabs block look active if any panel is targeted */
.tabs:has(.tabs__panel:target) {
  border-color: #2f80ed;
}

I am not pretending this scales to 50 tabs. For most content UIs, 3 to 5 tabs is realistic. Writing those few selectors is still cheaper than adding a tab manager, handling history state, and worrying about hydration.

The key pattern is: some child panel already has state via :target or [aria-selected="true"]. Let :has() bubble that state up to parents and siblings.

Accordion with native <details> and :has()

I use <details> a lot. It is surprisingly powerful with :has(). On a settings panel I wanted the container to visually compress when no section was open, then expand once any accordion entry was open.

<section class="settings">
  <details class="settings__item">
    <summary>Profile</summary>
    <div>...</div>
  </details>
  <details class="settings__item">
    <summary>Privacy</summary>
    <div>...</div>
  </details>
</section>

CSS:

.settings {
  padding: 1rem;
  border-radius: .75rem;
  border: 1px solid #333;
  max-height: 60vh;
  overflow: auto;
  transition: box-shadow .2s ease, border-color .2s ease;
}

.settings:has(.settings__item[open]) {
  border-color: #2f80ed;
  box-shadow: 0 16px 40px rgba(0, 0, 0, .55);
}

.settings__item + .settings__item {
  border-top: 1px solid #222;
}

.settings__item summary {
  cursor: pointer;
}

Once any <details> is open, the whole settings block feels “in focus”. No JS to listen for the toggle event, no syncing of .is-active classes. The HTML already has [open]. CSS reacts.

3. Form validation UI with zero JavaScript

The biggest win for me: form UI that uses :has() with built-in browser validation. No client-side validation library. No “touched” state juggling.

On my own site I revamped a contact form and a simple experiment log form. I wanted:

  • Parent field wrappers that highlight error or success
  • Inline messages that only show when actually invalid
  • Submit button that changes state based on form validity

Browser validation already tracks validity. The DOM knows. :has() lets CSS hook into that.

Field states from input validity

Markup:

<form class="form" novalidate>
  <div class="field">
    <label>
      Email
      <input type="email" name="email" required>
    </label>
    <p class="field__error">Please enter a valid email.</p>
  </div>

  <div class="field">
    <label>
      Message
      <textarea name="message" minlength="10" required></textarea>
    </label>
    <p class="field__error">Write at least 10 characters.</p>
  </div>

  <button type="submit">Send</button>
</form>

You can bind field styling to the input inside, purely with CSS.

.field {
  margin-bottom: 1.5rem;
}

.field input,
.field textarea {
  width: 100%;
  padding: .6rem .75rem;
  border-radius: .4rem;
  border: 1px solid #444;
  background: #050505;
  color: #eee;
}

.field__error {
  display: none;
  margin-top: .35rem;
  font-size: .8rem;
  color: #ff6b6b;
}

/* highlight when invalid and touched (using :user-invalid where supported) */
.field:has(input:user-invalid),
.field:has(textarea:user-invalid) {
  color: #ff6b6b;
}

.field:has(input:user-invalid) input,
.field:has(textarea:user-invalid) textarea {
  border-color: #ff6b6b;
  box-shadow: 0 0 0 1px rgba(255, 107, 107, .6);
}

.field:has(input:user-invalid) .field__error,
.field:has(textarea:user-invalid) .field__error {
  display: block;
}

/* success state */
.field:has(input:user-valid),
.field:has(textarea:user-valid) {
  color: #4caf50;
}

.field:has(input:user-valid) input,
.field:has(textarea:user-valid) textarea {
  border-color: #4caf50;
}

No custom event handlers. The browser decides when the input is valid or invalid. CSS uses :has() to move that state to the wrapper and the message.

If you want broader support than :user-invalid, you can fall back to :invalid and accept that some browsers show the state earlier.

Form-level feedback and submit button state

Now zoom out one level. The entire <form> element also exposes validity via :valid and :invalid. Combine that with :has() and your submit button can react.

.form button[type="submit"] {
  padding: .7rem 1.25rem;
  border-radius: .4rem;
  border: none;
  background: #333;
  color: #aaa;
  cursor: not-allowed;
  transition: background .15s ease, color .15s ease, transform .05s;
}

/* any invalid field keeps button in "disabled" style */
.form:has(:invalid) button[type="submit"] {
  background: #333;
  color: #777;
}

/* all fields valid, button goes live */
.form:has(:valid) button[type="submit"] {
  background: #2f80ed;
  color: #fff;
  cursor: pointer;
}

.form:has(:valid) button[type="submit"]:active {
  transform: translateY(1px);
}

If you want to actually disable the button, you still need a tiny bit of JS to toggle the disabled attribute. I usually do not bother for simple forms; the button just looks inactive until the browser considers the form valid.

The nice part is that the logic lives where it belongs. The browser enforces constraints. CSS reads that state. JS, if present at all, sends the request and displays a toast.

4. Layout tweaks based on children, not breakpoints

One more pattern that has crept into my “default toolkit”: adjusting layout based on how many items a container has.

On my baseball drills page, each drill has one or more tags. I wanted single-tag drills to show the tag inline next to the title, and multi-tag drills to move them into a separate row. Doing that in JS felt silly.

<article class="drill">
  <header class="drill__header">
    <h3 class="drill__title">Front toss</h3>
    <div class="drill__tags">
      <span class="tag">Hitting</span>
    </div>
  </header>
</article>

<article class="drill">
  <header class="drill__header">
    <h3 class="drill__title">Relay race</h3>
    <div class="drill__tags">
      <span class="tag">Fielding</span>
      <span class="tag">Conditioning</span>
    </div>
  </header>
</article>

With :has() and the :nth-child() selector you can treat the two cases differently.

.drill__header {
  display: flex;
  gap: .5rem;
  align-items: baseline;
  flex-wrap: wrap;
}

/* one tag only: keep inline */
.drill__tags:has(.tag:nth-child(1):last-child) {
  order: 0;
}

/* more than one tag: push tags to next line */
.drill__tags:has(.tag:nth-child(2)) {
  flex-basis: 100%;
  order: 1;
}

No JavaScript counting nodes. No data attributes. Just “if there is at least a second tag, change layout”. If product decides to add a third or fourth tag, the CSS keeps working.

Reality check: performance and support

I am not going to pretend :has() is free. The browser has to do more work, because selectors now depend on what is inside elements and how that changes.

My take after profiling a few real pages: do not go wild with global *:has(...) selectors. Scope them. Prefer direct children or close relationships.

/* Bad idea */
*:has(.error) { ... }

/* Reasonable */
.form:has(.field__error) { ... }

/* Even better */
.form:has(.field > .field__error) { ... }

Support is good now. Chrome, Edge, Safari, Firefox all ship :has(). Old Safari versions are the main risk. If you work on something critical for a weird enterprise fleet, check caniuse and add progressive enhancement.

Most of my patterns above fail gracefully. You lose a highlight or a layout tweak, not core functionality. That is a good bar to aim for.

How I think about :has() now

I used to reach for JavaScript whenever a parent needed to know about a child, or a sibling needed to react to state. That felt normal. It also created a lot of glue code that did not age well.

Now my filter is simple:

  • Is the state already visible in the DOM? (attribute, pseudo class, anchor, etc.)
  • Can that state reasonably drive styling only?

If the answer is yes, I try :has() first. JS comes later, if at all.

Five years ago I was writing tab managers and form validators by hand. I would not go back. :has() is the layout trick that finally lets CSS act on the structure we already have, instead of the utility classes we wish we had planned better.

If you have a component that keeps growing event listeners and state flags, look at the HTML for five minutes. There is a decent chance :has() can take some of that weight off.

You’re Renting Someone Else’s Compute — And It’s Costing You More Than You Think

Your Claude response comes back in 800 milliseconds. You’re on a roll. Three features shipped before lunch. And somewhere, silently, your debugging intuition is going to sleep.

I’ve been tracking a pattern across developer forums — not just V2EX, but in the back-channels of engineering team chats: developers who live in network-restricted regions are increasingly “renting” computational presence elsewhere. A computer in a data center, a VM in Singapore, a colleague’s spare workstation. They connect, they code, they use AI tools that would otherwise be unreachable. Problem solved.

Except it’s not solved. It’s deferred. And the cost is accumulating in a place most devs never check: the gap between what they can describe doing and what they can actually do.

The Compute Rental Economy

The V2EX discussion that triggered this article described a developer’s setup: living abroad, rented room with a desktop computer inside China, wants to remotely access that machine to use Claude’s web interface and write code. The comments branched into VPN recommendations, remote desktop protocols, browser-based solutions, and one or two voices asking the question nobody else wanted to answer — why?

The why matters. If you’re routing through a remote machine just to access an AI assistant, you’re not solving a network problem. You’re renting computational sovereignty. And like all rentals, you’re paying for access without building ownership.

Here’s what that looks like in practice, from a commenter’s description that stuck with me: a developer in Shanghai spends 4 hours daily on a remote desktop session to a machine in Tokyo. The latency hovers between 40-80ms — annoying but workable. The AI tools load. The code ships. And every evening, the developer closes the session knowing they built something without ever touching the actual hardware that built it.

That distinction — built on versus built with — is where the skill erosion starts.

Skeleton Implementation Syndrome

I need to coin a term here, because the existing vocabulary doesn’t capture this:

Skeleton Implementation Syndrome — the tendency to ship code you could describe but couldn’t write from scratch. You understand the architecture. You can explain why the service mesh routes requests the way it does. But when the AI is gone and the remote session drops, the gap between concept and implementation becomes a chasm you didn’t notice until you had to cross it alone.

This is different from normal abstraction. Normal abstraction is healthy — you don’t need to remember register allocation when writing Python. Skeleton Implementation Syndrome is pathological: you’ve delegated so much implementation to AI assistance that your mental model of how things actually work has decayed faster than your ability to ship features.

The ratio of regret here is asymmetric in a way that hurts quietly: AI assistance accelerates feature delivery (OPTIMIZED FOR) while accelerating capability decay (SACRIFICED). You win the sprint. You lose the skill. And the debt compounds invisibly because nobody measures “debugging intuition remaining” in your quarterly review.

The Local Environment Learning Tax

Here’s where I need to make an unpopular argument, and I want you to stay with me:

Running AI tools locally — even with degraded performance — produces better engineers than accessing them remotely on optimized infrastructure.

Before you close this tab: I’m not saying remote access doesn’t work. I’m saying the learning tax of renting compute is asymmetrically borne by the developer’s capability, not by their feature velocity.

When you run a model locally (even a quantized 7B parameter model that takes 45 seconds to warm up on your M2 Max), you’re forced to develop intuition about:

  • Token budgets and context windows — because you see the cost in real time, not abstracted away
  • Prompt sensitivity — because small changes produce observable differences without a slick web interface smoothing the edges
  • Failure modes — because local models fail in ways remote APIs don’t (OOM crashes, context truncation, hallucination patterns specific to your hardware)
  • System integration — because getting a local model to talk to your IDE requires actual configuration work, not just clicking “authorize”

The V2EX developer’s setup — remote machine, AI through a browser, code in a remote session — sidesteps all of this. The AI becomes a utility, like electricity. And like electricity, you stop thinking about how it works.

The Infrastructure You’re Betting On

There’s a second-order risk nobody talks about in these remote access discussions: you’re building workflow dependencies on infrastructure you don’t control.

Your remote machine exists because someone else maintains it. The network path between you and it exists because someone else routes it. The AI service you’re accessing exists because a company decided it should, and can decide otherwise.

In my local environment (M2 Max, 32GB RAM), I’ve been running a mix of local models and API access for two years. The local models are slower. They have smaller context windows. They fail in embarrassing ways. And they have never, not once, changed their terms of service, raised their prices, or decided my use case wasn’t “enterprise enough.”

The developers routing through Tokyo data centers to access Claude? They’re one corporate decision away from rebuilding their entire workflow. That’s not paranoia — that’s operational risk with a specific name: vendor dependency disguised as infrastructure convenience.

What Actually Survives

If you’re in a network-restricted region and remote access is genuinely your only option, I’m not here to tell you to suffer. Suffering isn’t a virtue. But here’s what I’d ask you to track, because I’ve watched this pattern destroy capable engineers:

Track your AI dependency score. After every coding session, ask yourself: could I have solved this without the AI? If the answer is “no, and I couldn’t have solved it six months ago either,” that’s data. That’s the gap growing.

The developers who survive this environment — who maintain capability while using AI as a multiplier — are the ones who treat AI as a colleague who happens to be infinitely patient, not a replacement for the thinking that made them dangerous in the first place.

They ask AI for second opinions, not first drafts. They use it to explore unfamiliar territory, not to avoid territory they should have mapped already. They keep a “dumb project” — something they code without AI, where inefficiency is the point, where the slow path is the learning path.

The Question I Can’t Answer For You

Here’s what I keep coming back to: the V2EX developer asked how to access Claude from their remote setup. Nobody asked what you’ll lose by making it this easy.

I don’t know your specific context. Maybe the feature velocity matters more than the debugging intuition. Maybe you’re in a sprint that doesn’t have room for the local model learning curve. Maybe the tradeoff is genuinely worth it.

But I know this: the engineers who lasted 15 years in this industry didn’t do it by shipping faster. They did it by being the person who could debug what everyone else gave up on. That capability doesn’t come from prompt engineering courses. It comes from struggling through problems without a safety net — and your remote setup, however clever, is a very comfortable safety net.

What’s the last thing you debugged without AI assistance? Not without searching the internet — without AI generating the answer for you. Go remember what that felt like. That’s the skill you might be renting away.

What’s your take?

Has your team noticed developers becoming less capable of independent debugging without AI? What’s your experience been — are you moving faster, or just shipping more?

I’d love to hear how this plays out in your specific context. Drop a comment below — I respond to every one.

Discussion on V2EX about remote access solutions for China-based developers wanting to use Claude web interface

Discussion: What’s the last thing you debugged without AI assistance — not without searching, but without AI generating the answer? How did it feel compared to using AI?

750,000 Chips, 140 Trillion Tokens: The Math Behind DeepSeek’s Permanent Price Cut

DeepSeek made its V4-Pro 75% price cut permanent on May 22. The conventional read: “they got cheaper hardware.” The real story is more interesting — and it’s about a gap that’s not closing fast enough.

What Happened

On May 22, 2026, DeepSeek announced that the 75% discount on its V4-Pro API would become permanent. The new pricing:

Metric Before After Cut
Input (cache miss) ¥12 / 1M tokens ¥3 / 1M tokens 75%
Output ¥24 / 1M tokens ¥6 / 1M tokens 75%
Input (cache hit) ¥0.1 / 1M tokens ¥0.025 / 1M tokens 75%

At current exchange rates, that’s roughly $0.44/M input and $0.87/M output — making V4-Pro one of the cheapest frontier-class models on the market, on par with DeepSeek’s own V4-Flash but with significantly more capability.

The move came exactly four weeks after V4’s launch on April 24, and coincided with growing user frustration over rate limits at Google Gemini and Anthropic Claude.

The Standard Narrative

The surface-level story has three parts:

1. Architectural efficiency. V4 uses a Mixture-of-Experts architecture with 1.6 trillion parameters, but only activates a fraction per token. This gives it a structural cost advantage over dense models of comparable capability — roughly 30% of the gap.

2. Supply chain scaling. Huawei’s Ascend 950PR entered mass production in April 2026. Huawei plans to ship ~750,000 units through the year — a 2.5x increase over 2025’s 910C output. DeepSeek specifically optimized V4 for the Ascend architecture. More chips → lower unit cost → lower API pricing.

3. Competitive positioning. Western AI providers (Google, Anthropic) have been quietly tightening rate limits as demand overwhelms their GPU supply. DeepSeek is exploiting the backlash, offering unlimited usage at a fraction of the cost to capture disgruntled developers.

All three are true. But none of them fully explains the magnitude of the cut — or why it’s permanent rather than promotional.

The Math That Changes Everything

Let’s check the numbers.

Demand Side

China’s daily token consumption hit 140 trillion in March 2026, according to the National Data Administration. The growth trajectory:

  • Early 2024: 0.1 trillion/day
  • End of 2025: 100 trillion/day
  • March 2026: 140 trillion/day

That’s a 1,000x increase in two years, and a 40% jump in just the last quarter — implying ~13% month-over-month growth.

Supply Side

Huawei’s mass-produced chip for 2026 is the Ascend 950PR (Prefill-optimized, 1 PFLOPS FP8), with the higher-end 950DT (2 PFLOPS FP8) coming in Q4. The numbers:

Chip FP8 Memory Bandwidth Inference Throughput (est.)
950PR 1 PFLOPS 128GB HBM 1.6 TB/s ~1,200 tokens/sec
950DT 2 PFLOPS 144GB HBM 4 TB/s ~2,400 tokens/sec

(Throughput derived from Huawei’s published Atlas 950 SuperNode benchmark: 19.6M tokens/sec across 8,192 cards.)

Now the arithmetic:

Item Value
Total chips (2026 target) 750,000 (70% PR + 30% DT)
Raw daily throughput 85.7 trillion tokens/day
Inference-allocated (60%) 51.4 trillion tokens/day
vs Current demand (140T) 37% coverage
vs Demand in 6 months (~291T) 18% coverage

Even in the most optimistic scenario — every single chip dedicated to inference at 100% utilization:

Scenario vs Current vs +6 months
100% inference, 100% utilization 61% coverage 29% coverage

The conclusion is stark: 750,000 Ascend 950 chips can’t cover today’s demand — let alone the demand in six months.

So Why Cut Prices?

If supply is still a fraction of demand, permanent price cuts don’t make sense in a normal market. But this is not a normal market.

The Real Logic: Pre-Commitment, Not Surplus

DeepSeek is not cutting prices because it has spare compute. It’s cutting prices to lock in routing commitments before the hardware arrives.

Here’s the timeline:

April 24:  V4 launched, optimized for Ascend
April 24+: ByteDance orders 350,000 Ascend 950 chips (~¥40B)
May 4:     Ascend 950PR mass production confirmed
May 22:    DeepSeek makes V4-Pro 75% cut permanent

The critical insight: DeepSeek’s price cut is not a cost pass-through. It’s a market share pre-commitment — using the promise of future Ascend supply to grab developer mindshare now, before Western competitors can resolve their own capacity issues.

The Numbers Behind the Strategy

Western providers are capacity-constrained:

Provider Constraint Signal
Google Gemini TSMC CoWoS capacity Rate limits tightened, user backlash
Anthropic Claude H100/B200 availability API throttling, compute-use monitoring
OpenAI Inference cluster rollout Delayed GPT-5 token limits

DeepSeek’s bet: “Spend the next 6 months building developer dependency on V4-Pro’s API — by the time Ascend supply catches up in H2 2026, those developers won’t switch back.”

This is AWS in 2006. AWS wasn’t cheaper than running your own servers in 2006. But it would be once scale kicked in. AWS priced for the scale it planned to have, not the scale it had. DeepSeek is doing the same.

What 750,000 Chips Actually Buys

The popular framing in Chinese media is “75万颗昇腾950产能大爆发.” But as the math shows, 750,000 chips isn’t abundance — it’s barely adequacy.

Think of it this way: China’s token demand is growing at roughly 0.5 trillion tokens per day every single month (the monthly increment itself is larger than the entire market 18 months ago). By year-end, demand will be 300-400+ trillion. Against that, 750K chips at the 950PR/DT mix buy roughly 50-85T/day of inference capacity.

Timeframe Demand (est.) Inference Supply Gap
March 2026 140T ~50T 90T
June 2026 ~200T ~50T 150T
September 2026 ~290T ~55T (DT ramp) 235T
December 2026 ~420T ~65T 355T

The gap is growing, not shrinking. Even with 75万 chips fully deployed, the supply-demand deficit more than triples over nine months.

This means DeepSeek’s price cut isn’t a sign of market saturation. It’s a sign of exactly the opposite: a market so unsaturated that the winner gets to define the default API for an entire generation of developers, if they can lock them in before the hardware arrives.

Three Counter-Arguments (And Why They’re Weak)

“But cache hits reduce the effective compute needed”

True — cache-hit tokens cost ~1/100th of miss tokens. And DeepSeek’s cache hit rates can be high for workloads with stable system prompts. But cache hits are mostly in the input direction. Output tokens — the expensive ones — still need full compute. And as agentic workloads grow (multi-turn, chain-of-thought), output-to-input ratios increase, making cache less effective.

“But not all 140T tokens need 950-class inference”

Also true. Many tokens are generated by smaller models (Flash variants, Qwen, etc.) that don’t need 950-level compute. But the growth is in the frontier-class tokens — longer context, more complex reasoning, higher quality requirements. That’s exactly where 950-class chips are needed.

“But they can still buy H20 / smuggled H100”

H20 is less capable than 950PR per chip (the US-designed it to be worse). And the CHIPS Act + export controls have made H100 procurement increasingly difficult. Relying on smuggled hardware is not a supply chain strategy.

What This Means

For Developers

Your inference costs are likely going down over the next 12 months, not up — even though demand is exploding. That’s unprecedented in any computing market. The driver isn’t efficiency gains or manufacturing scale. It’s a strategic subsidy by Chinese AI firms betting that locking in your API calls today is worth negative margins for a year.

Take the subsidy. But don’t assume today’s prices reflect tomorrow’s costs — they reflect tomorrow’s hopes.

For the Industry

The AI API market has entered a phase that looks like price war but functions like infrastructure land-grab. The playbook is AWS 2006, DoorDash 2019, Uber 2015: lose money on every transaction to own the default routing.

When the hardware does catch up — when Ascend 960 (2027) or 970 (2028) ships with 3-5x the throughput — the providers with the largest captive developer bases will convert negative margins to positive ones. Everyone else will be competing on price against incumbents they can’t dislodge.

The Bottom Line

DeepSeek’s permanent price cut is not evidence that Chinese AI compute supply has caught up with demand. The math shows it hasn’t — and won’t for at least 12-18 months. It’s evidence that DeepSeek is playing the long game: use today’s negative margins to own tomorrow’s default inference route, and trust that Huawei’s future chips will eventually close a gap that’s currently 3-5x wider than headlines suggest.

The 75% cut isn’t a cost breakthrough. It’s a bet that developer lock-in is worth more than current margins — and that the 75万 Ascend 950 chips shipping this year are just the beginning.

Numbers sourced from: National Data Administration (China daily token data, March 2026), Huawei Connect 2025 (Ascend 950 specs and roadmap), SCMP/DW (ByteDance order volume), DeepSeek official pricing page (May 2026). Throughput calculations based on published Atlas 950 SuperNode benchmarks. Growth projections assume continuation of 40%/quarter rate per published data.

What Happens When You Give AI Agents the Map of Your Code’s Coverage?

When you ask an AI agent to write a new feature, a good agent will eventually say: “I need to write a test for this.”

But what happens next is usually messy.

To figure out where that new test belongs, the agent has to start searching through your project. It might scan file names, inspect folders, grep for method names, and read file after file just to understand how your tests are organized. That burns through your token limits quickly.

Even worse, in complex .NET solutions where a single production file may be tested across multiple projects or test suites, the agent can still guess wrong. It might put the test in the wrong place, miss the relevant test fixture, or follow a completely different testing style from the one your team already uses.

In the Rider 2026.2 EAP, we’re improving this workflow by teaching AI agents a new skill – one that leverages JetBrains tooling for .NET coverage data to slash your AI expenses in half. 

Why AI agents need more than just code context

AI agents are useful because they can work through multi-step development tasks. They can inspect code, make changes, run tests, react to failures, and iterate.

But even capable agents are only as good as the context they receive.

When an agent needs to add a test, the question is not only: “What should this test assert?” It also needs to know:

  • Where do tests for this code usually live?
  • Which existing tests already exercise nearby code?
  • What testing framework, fixture structure, naming convention, and assertion style does this project use?
  • Is there already a test file that should be extended instead of creating a new one?

A human developer often knows this from experience. An AI agent usually has to discover it the expensive way: by reading the entire project.

That’s where AI agent skills come in.

What are agent skills?

If you’ve been following developments in AI-assisted development, you may have come across the concept of Agent Skills – an open standard introduced by Anthropic to extend AI agent capabilities with specialized knowledge and workflows. You can learn more about agent skills in JetBrains IDEs from this blog post.

In Rider, skills give AI agents access to IDE-native context and workflows. Instead of relying only on generic documentation or asking the model to explore your project manually, a skill can help the agent perform a specific task with information Rider already understands.

You can manage skills in Rider from Settings / Preferences | Tools | AI Assistant | Skills. Skills can be enabled in different scopes depending on how and where you want to use them:

  • IDE scope: Available for all projects and all agents inside the Rider UI. 
  • Global per-agent scope: Available for a specific agent across all projects, including outside the IDE. For example, this can be configured in an agent-specific directory such as ~/.codex.
  • Per-project scope: Available for all agents working with a specific project, including outside the IDE. For example, this can be configured in a project-level .agents directory.
  • Per-project per-agent scope: Available only for a specific agent in a specific project, such as a project-level .codex directory.

Once installed, skills can be used by supported agents automatically when they are relevant to the task. You can also invoke a skill manually in the AI chat by typing / followed by the skill name.

Enter the finding-tests skill

While ecosystems like Microsoft’s dotnet/skills give AI agents generic documentation on how to write .NET tests, they still leave the AI guessing where to put them.

Instead of letting the agent aimlessly search your codebase whenever it needs to write a test, we’ve introduced a new agent skill called finding-tests. It ships in two parts: as a bundled skill for Rider’s AI Assistant, and as a standalone MCP tool (findTests) for use with external agents like Claude Code or Codex.

The bundled skill is enabled by default in Rider’s AI Assistant in the Rider 2026.2 EAP. To use it with an external agent, install the skill in the relevant agent or project scope, then make sure the external client can access Rider via MCP. You can do this from Settings / Preferences | Tools | MCP Server: enable the MCP server, then use Auto-Configure to set up access for the external client.

The idea is simple: Rider already has access to powerful .NET coverage analysis through the bundled dotCover tool,  so when an AI agent needs to understand where a piece of code is tested, it should not have to infer that relationship solely from folder names and search results.

It can just ask Rider.

Rider will then use dotCover coverage data to identify which tests are connected to the code the agent is working on. That turns coverage data into actionable context for AI-generated tests.

Here is what the workflow looks like under the hood:

  1. The agent decides it needs a test. When the AI writes new code, modifies existing behavior, or you explicitly ask it to add test coverage, it can trigger the finding-tests skill.
  2. Rider asks dotCover for coverage context. dotCover runs the tests included in the solution and maps out the coverage data around the code the agent is working with.
  3. The right test location is found. Because Rider can understand which tests already cover nearby code, it can provide the agent with the relevant test file path instead of making the agent discover it manually.
  4. The agent follows your existing test style. The agent goes directly to the correct file, reads the surrounding tests, follows your project’s conventions, and generates a test that fits the existing codebase.

And here’s what the full workflow looks like inside the IDE:

The result: Token costs are halved

This isn’t just a quality-of-life improvement. It has a major impact on your workflow – and your budget.

✂️ Token costs are cut by 50%.

By stopping the AI from wandering needlessly through your project, you’d be saving drastic amounts of money.

In our internal benchmarks (primarily testing with the Claude agent), routing the agent directly to the correct file could cut token consumption by up to 50%. 

Cost comparison across a range of C# test generation cases in real open-source solutions shows that using the finding-tests agent skill with Claude can significantly reduce average AI agent task costs.

The benefit is not just lower AI spend. For teams working with quota-based access or shared AI credit pools, it also means fewer credits wasted on search and exploration. Instead, more of your AI allowance can go toward higher-value development tasks. 

🎯 Correct file, every time

Without coverage data, the agent guesses. In a large codebase where one class might be tested from multiple locations, those guesses are often wrong, resulting in tests dropped in the wrong file, written in a mismatched style, or targeting the wrong test suite entirely.

With finding-tests, the agent has a precise map. No guessing. No wrong file. No style mismatch.

Time vs. tokens

We want to be completely transparent about how this works: this setup trades token expenditure for time. 

To give the AI the exact file path, dotCover has to run a coverage analysis on your solution. For a small or medium project, this might take 30 seconds. But if you are working in a massive codebase, running a full coverage scan could take minutes or even hours.

If you have a release deadline tomorrow, the last thing you want is your IDE suddenly initiating a multi-hour test run. Luckily, the solution is fairly simple.

How to turn it off

Because of this time trade-off, putting you in control is our top priority. The finding-tests skill is bundled and enabled by default in this EAP, but you can disable it or keep it limited to specific projects.

To manage it, go to Settings / Preferences | Tools | AI Assistant | Skills. Find the finding-tests on the panel, and you can inspect the skill, disable it, or use the dropdown to configure it for specific projects.

You can easily re-enable the skill later. 

Known issues

As this is an Early Access Program (EAP), we’re still ironing out a few wrinkles. Here is what you should look out for:

  • First-run hiccups: The finding-tests tool occasionally stumbles or fails on its very first launch. A second attempt usually ensures it is on track.
  • Codex support in the IDE is currently limited: At the moment, bundled skills are not available for Codex inside AI Assistant because of a known issue with the skill bundling mechanism. The AI Assistant team is working on a fix.

Possible workaround: If you want to use finding-tests with Codex, install the skill explicitly in either the global or project scope. This makes the skill available to both external and in-IDE agents.

  • Timeouts on large solutions: Codex and Copilot agents currently time out if a full test run takes longer than 120 seconds. We know real-world solutions can take longer to test, and we’re working on optimizing this pipeline.

    Workaround for Codex: External Codex can mitigate this by increasing the tool timeout in the MCP configuration. Set a larger value for the tool_timeout_sec parameter in the global or project MCP config as per Codex documentation.

  • External agents require MCP setup: To use finding-tests with external agents such as Claude Code or Codex, you’ll need to enable Rider’s MCP server and configure MCP access for the agent.
    Go to Settings / Preferences | Tools | MCP Server, enable the MCP server, and then either auto-configure or manually configure Rider’s MCP server for your agent. After that, install the skill in the agent’s global or project scope.

What’s next on the roadmap

Here’s a sneak peek at where this is heading: if the finding-tests skill proves valuable, our next step will be introducing target coverage – a feature where the AI agent automatically generates enough unit tests to hit a specific, pre-selected percentage of code coverage.

This would let you easily meet mandatory coverage requirements without having to spend extra time manually writing tests. Your feedback on this EAP directly influences whether this feature gets built.

Tell us what you think

This feature is available to all users in the Rider 2026.2 EAP, which gives you a good opportunity to also explore dotCover – our star code coverage tool that normally requires a dotUltimate license – for free. 

Download Rider 2026.2 EAP

For now, we need to know whether this skill works and provides enough benefits for you, and whether there are any edge cases we need to take into account. Try out the new test generation, see how it impacts your workflow, and please let us know whether this skill should remain bundled by default or moved to an optional registry!

The JetBrains Fit Test: Is This the Right Workplace for You?

If you’ve ever wondered what it’s really like to work at JetBrains, this post is for you.

We could tell you about our products, our offices, or the number of developers who use our tools, but the truth is, the real story of JetBrains is about the people who build those tools, the way they think, and what drives them.

This isn’t a traditional “why you should join us” piece. It’s more of a fit test – a look inside the mindset, values, and rhythm that shape daily life here. Because we believe that finding the right workplace isn’t just about skills, it’s about resonance. It’s about whether you’ll feel at home with our way of working.

So let’s see if you do, together. At JetBrains, everything we do is shaped by a few simple yet powerful ideas; these principles guide how we build, grow, and work as a team.

Built by developers, for developers – and everyone who shares that mindset

JetBrains was founded by developers who wanted to make software creation more productive, logical, and human-focused. “By developers, for developers” isn’t just a slogan. It defines how all of our teams think and build – not just our engineers.

We create tools we use ourselves. We test them on real projects, feel the same pain points our users do, and fix what doesn’t work. Something that’s know also as dogfooding.

This constant loop of using, improving, and learning keeps us close to the people we build for.

And while our roots are in software development, this mindset goes far beyond code – it’s shared by designers, marketers, recruiters, writers, and every JetBrainer who approaches their craft with the same curiosity, precision, and care.

“Every feature, every product we create is made with love for people like us, people who care about doing things right”,

as our CEO, Kirill Skrygan, put it.

That closeness to our users also explains our independence. We’re privately owned, and that’s deliberate. Without external investors, we can focus on real value instead of chasing hype or short-term profit. It’s what allows us to take our time, build with care, and stay loyal to our craft.

If that kind of depth and autonomy excites you, you’ll probably feel right at home here.

The relentless pursuit of getting it right

We care deeply about details. When you build tools for some of the smartest and most demanding users in the world, “good enough” simply isn’t going to work for us.

Every role at JetBrains, from designers to developers, from lawyers to technical writers, carries that same sense of precision and pride. Titles matter less than skill, curiosity, and craftsmanship.

Here, decisions are made by doers, by people who know the work inside out. We ask hard questions, challenge assumptions, and don’t cut corners. It’s a place where excellence attracts excellence: Great people want to work with others who raise the bar.

That’s why our feedback culture can feel intense at first. JetBrainers are direct and deeply invested in quality. But the goal is always to make our work, and each other, better.

If you find joy in perfecting something, if you feel satisfaction when a system works beautifully because you made it that way, you’ll probably enjoy this environment.

Freedom to forge

Autonomy is one of the defining traits of JetBrains. We trust capable people to lead themselves.

But autonomy is not for everyone. For some, it is energizing. For others, it can feel disorienting. Not everyone feels comfortable without clear direction, and that is okay.

Here, you won’t be handed a step-by-step plan. You will not get detailed instructions for every move. Instead, you are given trust and the space to figure things out. To question the default. To experiment. To choose a better path when you see one.

As Kirill once said,

“JetBrains is not the best place for people who want everything predictable and perfectly structured. It is the place for people who see something broken and just fix it.”

If you see a better way forward, you are empowered to take it. That kind of freedom can be thrilling, but it also means you own the outcome. Accountability is part of the deal.

If you feel at home in a flat structure, enjoy experimenting, and are motivated by trust and autonomy, JetBrains is a good fit for you.

Your career’s best move

People often join JetBrains for a role, but they stay for a career. Our attrition rate is less than half the industry average, and that’s not a coincidence.

Growth here doesn’t follow a straight corporate ladder. It’s more like a map you design yourself. Some deepen their expertise; others move into leadership, switch domains, or build something entirely new.

Teams are intentionally small, so every person’s work leaves a visible mark. When you contribute, you see the impact immediately: on products, users, and your colleagues.

Recognition here isn’t performative; it’s real. We take care of our people, and our top performers know their contribution is valued, both in recognition and in compensation.

“When you give smart people the space to grow, they push boundaries of what’s possible, and of who they can become,” said Kirill.

If you’re someone who’s motivated by growth, ownership, and the freedom to keep evolving, this is a place where your career can keep expanding without having to start over every few years.

What kind of people thrive at JetBrains?

There’s no single “JetBrainer type”. But the people who thrive here share a certain mindset, one that shapes how they work, grow, and collaborate every day.

They raise the bar.
They don’t just get the job done; they elevate the standards for everyone around them. They bring real skill, a thoughtful approach, and an eye for the details that matter.

They care deeply about the “why”.
They’re driven by the opportunity to solve hard problems and build something that truly matters, not just to deliver, but to make an impact.

They don’t settle for “good enough”.
They dig in, challenge assumptions, and find new paths forward because they care about getting it right.

They’re proactive and accountable.
They make decisions, take ownership, and lead from wherever they are, regardless of their job title or position in the hierarchy.

They value independence, matched with integrity.
They hold themselves to high standards not because someone’s watching, but because their work matters to them.

They act with intent.
They move quickly when it counts, focusing on impact over “looking busy”, progress over noise.

And above all, they care: about the craft, their colleagues, and doing things properly.

A place for builders, believers, and doers

If there’s one phrase that captures the essence of working at JetBrains, it’s this: We’re a company led by doers. Here, decisions are made by people who know their work inside out and who aren’t afraid to take initiative. We’re looking for process shapers  – people who believe in what they build and want to see it come to life. 

This is not the easiest place to work. But for the right person, it might be the most fulfilling.

If you’re the kind of person who feels restless when things could be better, who starts tinkering, fixing, creating, and improving, you’ll find your people here.

Thinking of applying?

Do you want to do your best work and be surrounded by people who do the same? If your answer is yes, you might just fit right in. Check out our careers page to learn more.

Rider 2026.2 EAP 3: Cost-effective Agentic Test Coverage, Code Change Previews, GameDev Templates, and NuGet Improvements

JetBrains Rider 2026.2 EAP 3 is out!

You can download this version from our website, update directly from within the IDE, use the free Toolbox App, or install it via snap packages.

Download Rider 2026.2 EAP 3

Here’s what you can expect from this update:

New AI agent skill to reduce token use for test generation

We’re also experimenting with an AI agent skill for unit test generation that uses Rider’s built-in coverage data to produce more relevant tests. When you ask your AI agent to generate tests, Rider can use dotCover coverage insights to find existing related tests, follow your project’s testing style, and generate the perfect tests with no manual guidance or costly wandering round the codebase. In our internal benchmarks, this approach reduced token consumption by up to 50%. You’ll find more details in this blog post.

The ability to preview suggested code changes

Rider now gives you a clearer way to evaluate quick-fixes and context actions before applying them. The new intention previews show what the selected action will change directly from the actions menu, helping you understand the result at a glance and choose the right fix with more confidence.

The preview supports diff-based output with syntax and identifier highlighting, so you can quickly compare the before and after states without interrupting your flow. This is especially helpful for broader changes, including fixes that affect multiple files, where seeing the exact impact upfront makes code actions feel safer and easier to trust.

Game development project templates

Rider now includes a dedicated Game Development section in the New Project dialog, making it easier to get started without any overcomplicated manual setup.

Godot is the first pilot for this updated experience. You can create either a game extension or an editor extension, with options to include C++ GDExtension support and the CMake add-on manager (the JetBrains Rider add-on is preconfigured where relevant). It’s a faster path to a working project, especially if you’re new to Godot development in Rider.

This release also lays the groundwork for more game-specific templates in Rider. Alongside the Godot pilot, we’re introducing a CMake game project template and reorganizing the New Project experience so game development templates have a clearer, dedicated entry point.

If you would like to learn how to use the new templates and develop Godot addons, check our documentation.

Improved experience in the NuGet tool window

Managing dependencies can get noisy as a solution grows. You need to find new packages, keep existing dependencies up to date, and quickly understand which projects are affected by available updates – ideally without digging through the same package list over and over again.

We’ve redesigned the NuGet tool window in Rider to make that workflow easier to understand and act on. The updated experience separates browsing for packages from managing installed dependencies, so each task has its own clearer path.

Available updates now also have a dedicated place in the tool window, making it easier to see which packages need attention and update them when you’re ready. This should make routine dependency maintenance more focused, especially in larger solutions with multiple projects and many installed packages.

Optimized garbage collection in Rider’s backend

We’ve adjusted several garbage collection settings to help the Rider backend release unused memory more efficiently.

Based on our internal tests, these changes reduced memory usage for Rider backend processes by around 7–8% on average. Your results may vary depending on your project and environment, but Rider should now be better at managing backend memory during everyday development.

For the full list of improvements and fixes included in this build, please see our release notes.

Download Rider 2026.2 EAP 3

That’s it for now! As always, we’d love to hear your thoughts in the comments below.

Advanced Tree Counting: Mathematical Layouts With `sibling-index()` And `sibling-count()`

You know that thing where you have a grid of cards, and you want them to fade in one after another? That staggered cascade effect. Looks great. Should be simple. And yet every time I’ve built it, the implementation has made me feel like I’m doing something fundamentally stupid.

What’s Coming

The current spec only counts all element siblings. But the CSSWG has documented a planned extension in issue #9572: an of <selector> argument, matching what :nth-child() already supports.

Something like sibling-index(of .active) would let you count only siblings matching a specific selector. An element that’s the eighth child overall but the third .active child would return 3. For dynamic UIs where you’re filtering or toggling visibility, that would keep the index sequential without requiring DOM manipulation.

There’s also been CSSWG discussion around children-count() and descendant-count() functions — the first would tell you how many children an element has (useful for parent-driven layouts), the second would count all descendants recursively. Both are still at the proposal stage, but they’d round out the tree-counting story: sibling-index() and sibling-count() give you the horizontal view (where am I among my peers?), while children-count() and descendant-count() would give you the vertical view (what’s below me?).

That feeling I mentioned at the top — writing ten :nth-child() rules for a staggered animation and wondering if you’re missing something obvious? You weren’t. The obvious thing just didn’t exist yet.