<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dmitry</title>
    <description>The latest articles on DEV Community by Dmitry (@dmitryganin).</description>
    <link>https://dev.to/dmitryganin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3947986%2F42a773e0-215c-48aa-ac9b-48c1e9291ca6.jpeg</url>
      <title>DEV Community: Dmitry</title>
      <link>https://dev.to/dmitryganin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmitryganin"/>
    <language>en</language>
    <item>
      <title>Your Mac Has 80GB of Hidden Junk. Here's How to Find It.</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Thu, 11 Jun 2026 07:44:54 +0000</pubDate>
      <link>https://dev.to/dmitryganin/your-mac-has-80gb-of-hidden-junk-heres-how-to-find-it-543g</link>
      <guid>https://dev.to/dmitryganin/your-mac-has-80gb-of-hidden-junk-heres-how-to-find-it-543g</guid>
      <description>&lt;p&gt;After years of using CleanMyMac and watching it beg me for a paid upgrade every time I clicked something useful, I decided to build my own. Meet &lt;strong&gt;Trashly&lt;/strong&gt; — a macOS disk cleaner written in Rust with a React frontend, open-source (AGPL-3), and with zero upsells.&lt;/p&gt;

&lt;p&gt;This post is about why I built it, the interesting technical choices I made, and the one thing most cleaners get completely wrong.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu87zgzjjgey5yqjff4oj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu87zgzjjgey5yqjff4oj.png" alt=" " width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Cleaners Don't Know About Developers
&lt;/h2&gt;

&lt;p&gt;Mainstream Mac cleaners do a decent job on browser caches and basic junk. But if you're a developer? They leave gigabytes on the table.&lt;/p&gt;

&lt;p&gt;On my MacBook after about a year of development, I had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~40 GB&lt;/strong&gt; of Xcode &lt;code&gt;DerivedData&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~15 GB&lt;/strong&gt; of iOS DeviceSupport (archived device frameworks for debugging)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~20 GB&lt;/strong&gt; across &lt;code&gt;node_modules&lt;/code&gt; folders in abandoned projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~8 GB&lt;/strong&gt; of Android emulator images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~6 GB&lt;/strong&gt; of JetBrains caches&lt;/li&gt;
&lt;li&gt;Docker volumes I forgot existed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generic cleaners found maybe 2 GB of browser cache and called it a day.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;I wanted something that felt native and was actually fast. The choice was easy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rust&lt;/strong&gt; for the backend — memory safety, real parallelism, direct OS access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tauri 2&lt;/strong&gt; as the bridge — gives you a native window with WebView, IPC, tray icon, and auto-updates with no Electron overhead (~8 MB app bundle vs ~200 MB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React 19 + TypeScript&lt;/strong&gt; for the UI — familiar tooling, great ecosystem, and Tauri's WebView means I don't have to learn SwiftUI&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Interesting Parts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Safety-First Deletion
&lt;/h3&gt;

&lt;p&gt;The thing that terrifies me about disk cleaners is "what if it deletes the wrong thing." So the first thing I built was a strict allowlist guard in Rust:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;is_safe_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;allowed_roots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;home_dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Library/Caches"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;home_dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Library/Logs"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;home_dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".Trash"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;// developer paths...&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="n"&gt;allowed_roots&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.any&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="nf"&gt;.starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="nf"&gt;.as_path&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key detail: &lt;code&gt;path != root&lt;/code&gt; — you can't delete the root itself, only things &lt;em&gt;inside&lt;/em&gt; it. And &lt;code&gt;..&lt;/code&gt; escapes are rejected before this check even runs. This guard lives in Rust, not JavaScript. Even if a malicious path somehow came from the UI layer, the backend would refuse it.&lt;/p&gt;

&lt;p&gt;By default, everything goes to the Trash via &lt;code&gt;NSFileManager&lt;/code&gt; — permanent deletion is opt-in.&lt;/p&gt;




&lt;h3&gt;
  
  
  Streaming Scan Results
&lt;/h3&gt;

&lt;p&gt;Nobody wants to stare at a spinner for 30 seconds before seeing results. The scan fans out across all categories in parallel using &lt;code&gt;rayon&lt;/code&gt;, and results stream to the UI as they're found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;scan_caches&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ScanResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;mpsc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;rayon&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;scanner&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SCANNERS&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scanner&lt;/span&gt;&lt;span class="nf"&gt;.scan&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="nf"&gt;.blocking_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nn"&gt;ReceiverStream&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI renders file entries the moment they arrive. A 40 GB &lt;code&gt;DerivedData&lt;/code&gt; scan starts showing results within 200ms.&lt;/p&gt;




&lt;h3&gt;
  
  
  Finding Dead &lt;code&gt;node_modules&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is where Trashly gets opinionated. Rather than scanning for &lt;code&gt;node_modules&lt;/code&gt; anywhere on the disk, it looks for project manifests first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;find_project_artifacts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ArtifactResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;WalkDir&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.filter_map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;is_manifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.path&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="c1"&gt;// package.json, Cargo.toml, etc.&lt;/span&gt;
        &lt;span class="nf"&gt;.flat_map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;project_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="nf"&gt;.path&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.parent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;ARTIFACT_DIRS&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;project_dir&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="nf"&gt;.exists&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ArtifactResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
                &lt;span class="py"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It finds &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;target&lt;/code&gt;, &lt;code&gt;dist&lt;/code&gt;, &lt;code&gt;.next&lt;/code&gt; — but only when they sit next to a real &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;Cargo.toml&lt;/code&gt;, &lt;code&gt;build.gradle&lt;/code&gt;, etc. No false positives from unrelated folders with the same name.&lt;/p&gt;




&lt;h3&gt;
  
  
  Perceptual Photo Deduplication
&lt;/h3&gt;

&lt;p&gt;Exact duplicate detection (BLAKE3 hash matching) is table stakes. The interesting problem is &lt;em&gt;similar&lt;/em&gt; photos — the same screenshot at two export sizes, the same photo edited in Lightroom, burst shots that are 95% identical.&lt;/p&gt;

&lt;p&gt;I implemented dHash clustering:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decode image to 9×8 grayscale (via the &lt;code&gt;image&lt;/code&gt; crate; HEIC files go through system &lt;code&gt;sips&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;For each row, compute whether each pixel is brighter than its right neighbor → 64-bit hash&lt;/li&gt;
&lt;li&gt;Group images where Hamming distance &amp;lt; threshold into clusters&lt;/li&gt;
&lt;li&gt;Within each cluster, keep the largest file (assumed best quality) and offer the rest for deletion
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;dhash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;DynamicImage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;small&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="nf"&gt;.resize_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;FilterType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Lanczos3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                   &lt;span class="nf"&gt;.grayscale&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;pixels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="nf"&gt;.to_luma8&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pixels&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pixels&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;hash&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a photo library of 12,000 images, this runs in about 8 seconds on an M1.&lt;/p&gt;




&lt;h3&gt;
  
  
  System Metrics That Actually Work on Recent macOS
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;sysinfo&lt;/code&gt; crate — which most Rust system monitor tools reach for — reports zero memory pressure on macOS 14+. The kernel API it uses was deprecated.&lt;/p&gt;

&lt;p&gt;I ended up parsing &lt;code&gt;top -l 1 -n 0&lt;/code&gt; and &lt;code&gt;netstat -i -b&lt;/code&gt; directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;memory_pressure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MemPressure&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"memory_pressure"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// parses "System-wide memory free percentage: 42%"&lt;/span&gt;
    &lt;span class="c1"&gt;// and "The system memory pressure is: CRITICAL"&lt;/span&gt;
    &lt;span class="nf"&gt;parse_memory_pressure_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="py"&gt;.stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ugly? A little. But it returns the same values macOS shows in Activity Monitor, which is what users expect.&lt;/p&gt;




&lt;h2&gt;
  
  
  The App Uninstaller
&lt;/h2&gt;

&lt;p&gt;This is the feature I'm most proud of. When you drag an app to Trash on macOS, you leave behind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/Library/Application Support/&amp;lt;AppName&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/Library/Caches/com.example.app&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/Library/Containers/com.example.app&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/Library/Preferences/com.example.app.plist&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Launch Agents&lt;/li&gt;
&lt;li&gt;Browser extensions and profiles&lt;/li&gt;
&lt;li&gt;Crash reports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trashly hunts all of these down by bundle ID and developer name, lets you preview them, and optionally keep the app binary while wiping just its data (great for resetting an app to factory state).&lt;/p&gt;

&lt;p&gt;For developer tools (Xcode, Android Studio, JetBrains, Docker), there are dedicated cleanup routines that know exactly where each tool stores its caches and how much space each component takes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tauri is genuinely great.&lt;/strong&gt; The IPC ergonomics are excellent — you define a Rust function with &lt;code&gt;#[tauri::command]&lt;/code&gt;, and the TypeScript side gets a typed async function. Hot reload works. The app bundle is tiny.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rust's safety guarantees matter for this use case.&lt;/strong&gt; There are code paths in Trashly that walk the entire home directory and make deletion decisions. Having the compiler verify that I'm not accidentally aliasing mutable state across threads, and that every error is handled, is worth the learning curve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most disk usage surprises people.&lt;/strong&gt; The feedback I consistently get is: "I had no idea DerivedData was this big." Xcode will accumulate build artifacts for every simulator OS version and every version of your app it's ever compiled. 50 GB is not unusual for active iOS developers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to Find It
&lt;/h2&gt;

&lt;p&gt;Trashly is open-source under AGPL-3 on GitHub. No telemetry, no accounts, no upsells. The cleanup history is a local-only audit log so you can always see what was removed and when.&lt;/p&gt;

&lt;p&gt;If you're macOS user and you're regularly running out of disk space, give it a try. The first scan usually finds 30–80 GB on a machine that's been used for a year or more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AppsGanin/trashly" rel="noopener noreferrer"&gt;github.com/AppsGanin/trashly&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Rust, Tauri 2, React 19. Feedback welcome — especially if you know a cache directory I haven't found yet.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>opensource</category>
      <category>tauri</category>
      <category>programming</category>
    </item>
    <item>
      <title>Вечер на автоматизацию GitHub-ачивок</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Fri, 29 May 2026 15:20:58 +0000</pubDate>
      <link>https://dev.to/dmitryganin/viechier-na-avtomatizatsiiu-github-achivok-chto-poshlo-nie-tak-i-pochiemu-eto-intieriesno-3o5o</link>
      <guid>https://dev.to/dmitryganin/viechier-na-avtomatizatsiiu-github-achivok-chto-poshlo-nie-tak-i-pochiemu-eto-intieriesno-3o5o</guid>
      <description>&lt;p&gt;Началось всё банально: хотел собрать ачивки на GitHub-профиле.&lt;/p&gt;

&lt;p&gt;Процесс оказался несложным по сути, но раздражающим по исполнению — куча однотипных ручных шагов, ожидание ревьюеров, непрозрачные условия засчитывания. Типичная задача на «автоматизировать и забыть».&lt;/p&gt;

&lt;p&gt;Я взял её как мини-проект на вечер. Вечер растянулся.&lt;/p&gt;




&lt;h2&gt;
  
  
  Что хотелось получить
&lt;/h2&gt;

&lt;p&gt;Публичный репозиторий, куда любой может открыть PR, и он автоматически мёрджится — без мейнтейнера, без ожидания, без магии.&lt;/p&gt;

&lt;p&gt;Звучит просто. Но сразу встал вопрос: как сделать это &lt;strong&gt;безопасно&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;Открытый авто-мёрдж без ограничений — это либо спам, либо кто-то пропушит что-то лишнее. Нужна была система проверок.&lt;/p&gt;




&lt;h2&gt;
  
  
  Первое нетривиальное решение: &lt;code&gt;pull_request_target&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Для авто-мёрджа форков нужен доступ к секретам репозитория. Обычный &lt;code&gt;pull_request&lt;/code&gt; такого доступа не даёт — он намеренно изолирован для безопасности.&lt;/p&gt;

&lt;p&gt;Правильный ответ — &lt;code&gt;pull_request_target&lt;/code&gt;. Он запускается в контексте &lt;strong&gt;целевого&lt;/strong&gt; репозитория, а не форка, поэтому имеет доступ к токенам. Но это же делает его опасным, если не выстроить валидацию до любых действий.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request_target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contributors/**"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Вся логика валидации идёт &lt;strong&gt;первой&lt;/strong&gt;, до мёрджа — иначе это дыра.&lt;/p&gt;




&lt;h2&gt;
  
  
  Валидация: динамический regex из логина автора
&lt;/h2&gt;

&lt;p&gt;Я хотел, чтобы каждый пользователь мог добавить &lt;strong&gt;только свой&lt;/strong&gt; файл. Не чужой, не случайный — именно &lt;code&gt;username.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Жёсткий список заранее не составишь. Поэтому имя файла проверяется динамически: regex строится прямо из логина автора PR.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validPattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`^contributors/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;(-&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;d+)?&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.md$`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invalidFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;validPattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Паттерн допускает &lt;code&gt;username.md&lt;/code&gt;, &lt;code&gt;username-2.md&lt;/code&gt;, &lt;code&gt;username-3.md&lt;/code&gt; — для случаев, когда нужно несколько PR от одного пользователя. Всё остальное — отказ с понятным сообщением прямо в комментарии к PR.&lt;/p&gt;




&lt;h2&gt;
  
  
  Galaxy Brain: правильный ответ спрятан в HTML-комментарии
&lt;/h2&gt;

&lt;p&gt;Для ачивки Galaxy Brain нужно, чтобы кто-то ответил на вопрос в Discussions и ответ был помечен как правильный.&lt;/p&gt;

&lt;p&gt;Автоматизировать проверку ответа — задача нетривиальная. Хранить правильные ответы в отдельном файле/базе? Лишние сущности. Сравнивать с внешним API в момент ответа? Медленно и ненадёжно.&lt;/p&gt;

&lt;p&gt;Элегантное решение: правильный ответ прячется прямо в теле discussion в HTML-комментарии, который пользователь не видит, а workflow читает.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!--&lt;/span&gt; &lt;span class="nx"&gt;ANSWER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JavaScript&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Когда кто-то отвечает в треде, workflow проверяет, содержит ли комментарий нужную строку:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;answerMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;discussion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;!--&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*ANSWER:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;.+&lt;/span&gt;&lt;span class="se"&gt;?)\s&lt;/span&gt;&lt;span class="sr"&gt;*--&amp;gt;/i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;correctAnswer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;answerMatch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commentBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;commentBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;correctAnswer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// неправильный ответ — ничего не делаем&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Никакой внешней базы. Данные живут там, где они нужны.&lt;/p&gt;




&lt;h2&gt;
  
  
  Discussions API: только GraphQL
&lt;/h2&gt;

&lt;p&gt;GitHub REST API не поддерживает принятие ответа в Discussions. Это доступно только через GraphQL — &lt;code&gt;markDiscussionCommentAsAnswer&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mutation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
  mutation($commentId: ID!) {
    markDiscussionCommentAsAnswer(input: { id: $commentId }) {
      discussion { number url }
    }
  }
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;graphql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;commentId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Аналогично для создания самих вопросов и добавления реакций — всё через GraphQL. REST здесь просто не работает.&lt;/p&gt;




&lt;h2&gt;
  
  
  Идемпотентность: не создавать вопросы, пока есть неотвеченные
&lt;/h2&gt;

&lt;p&gt;Workflow создания вопросов запускается три раза в день. Но создавать новые вопросы, пока старые висят без ответа — бессмысленно и засоряет Discussions.&lt;/p&gt;

&lt;p&gt;Перед созданием — проверка:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unanswered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;discussions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unanswered&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// пропускаем этот запуск&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Запуск идемпотентен: повторный прогон не ломает состояние.&lt;/p&gt;




&lt;h2&gt;
  
  
  Схема всего
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PR pipeline:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User opens PR
  → pull_request_target
  → Validation
      ✅ pass → Squash merge
      ❌ fail → Comment with reason
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Galaxy Brain pipeline:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cron 3x/day
  → Unanswered questions exist?
      yes → skip
      no  → fetch from OpenTDB
           → create discussions with hidden ANSWER tag

New discussion comment
  → correct answer? → markDiscussionCommentAsAnswer
  → wrong answer?   → ignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Что получилось в итоге
&lt;/h2&gt;

&lt;p&gt;Репозиторий, где:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🦈 &lt;strong&gt;Pull Shark&lt;/strong&gt; — PR в &lt;code&gt;contributors/&lt;/code&gt; → авто-мёрдж&lt;/li&gt;
&lt;li&gt;👥 &lt;strong&gt;Pair Extraordinaire&lt;/strong&gt; — PR в &lt;code&gt;pairs/&lt;/code&gt; → авто-мёрдж с &lt;code&gt;Co-authored-by&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🧠 &lt;strong&gt;Galaxy Brain&lt;/strong&gt; — отвечай на вопросы в Discussions → бот принимает правильные&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Quickdraw&lt;/strong&gt; и 🤠 &lt;strong&gt;YOLO&lt;/strong&gt; — инструкции за 2 клика&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Без ожидания, без мейнтейнера, без магии.&lt;/p&gt;




&lt;h2&gt;
  
  
  Дисклеймер
&lt;/h2&gt;

&lt;p&gt;Проект создан в образовательных и развлекательных целях.&lt;br&gt;
Ачивки — косметика профиля, не метрика уровня инженера.&lt;/p&gt;

&lt;p&gt;Используй в рамках &lt;a href="https://docs.github.com/en/site-policy/github-terms/github-terms-of-service" rel="noopener noreferrer"&gt;GitHub Terms of Service&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Если идея понравилась — посмотри &lt;strong&gt;&lt;a href="https://github.com/AppsGanin/achievement-github-farm" rel="noopener noreferrer"&gt;GitHub Achievement Farm&lt;/a&gt;&lt;/strong&gt; и поставь ⭐, если зашло.&lt;/p&gt;

&lt;p&gt;Если есть вопросы по деталям реализации или нашёл edge-case — welcome в &lt;a href="https://github.com/AppsGanin/achievement-github-farm/issues" rel="noopener noreferrer"&gt;issues&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>github</category>
      <category>githubactions</category>
      <category>opensource</category>
      <category>automation</category>
    </item>
    <item>
      <title>Ultimate 1-Minute Xray/3x-ui Setup: VLESS, Hysteria2, Caddy Self-Steal &amp; Smart Outbounds in One Script</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Wed, 27 May 2026 20:51:45 +0000</pubDate>
      <link>https://dev.to/dmitryganin/ultimate-1-minute-xray3x-ui-setup-vless-hysteria2-caddy-self-steal-smart-outbounds-in-one-594d</link>
      <guid>https://dev.to/dmitryganin/ultimate-1-minute-xray3x-ui-setup-vless-hysteria2-caddy-self-steal-smart-outbounds-in-one-594d</guid>
      <description>&lt;p&gt;Setting up a secure internet circumvention gateway using Xray usually turns into a tedious chore. You have to install Docker, configure the 3x-ui web panel, generate TLS certificates, tweak Reality settings against active probing, and manually set up routing tables. &lt;/p&gt;

&lt;p&gt;If you frequently cycle your VPS providers or manage servers for friends, doing this manually every time is a massive waste of time.&lt;/p&gt;

&lt;p&gt;That’s why I created &lt;strong&gt;3xui-fast-install&lt;/strong&gt; — a lightweight, automated bash script that deploys a fully production-ready, security-hardened Xray node in under a minute.&lt;/p&gt;




&lt;h2&gt;
  
  
  What’s Under the Hood? 🛠️
&lt;/h2&gt;

&lt;p&gt;This is &lt;strong&gt;NOT just a blank panel installation&lt;/strong&gt;. The script sets up an optimized, batteries-included network ecosystem wrapped in Docker. Here is exactly what gets deployed and configured out of the box:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3x-ui Panel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Xray web management interface running on custom non-standard ports.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VLESS + Reality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Industry-standard transport layer mimicking legitimate TLS handshakes (Port 443).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hysteria 2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High-performance UDP-based protocol (Port 63000) optimized for unstable or mobile networks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Caddy (Self-Steal)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Acts as a TLS terminator on port 443, handling automated, legitimate Let's Encrypt certificates.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare WARP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Configured as a local SOCKS5 outbound proxy for specific fallback routing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Opera Proxy &amp;amp; Tor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Additional local SOCKS5 outbounds ready for geo-blocked services and &lt;code&gt;.onion&lt;/code&gt; routing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BBR &amp;amp; Security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automated TCP BBR congestion control activation, UFW firewall configuration, and Fail2ban protection.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Smart Server-Side Routing Architecture 🗺️
&lt;/h2&gt;

&lt;p&gt;One of the highlights of this setup is how it handles outbound traffic directly inside Xray on the server. Instead of just passing everything blindly (&lt;code&gt;direct&lt;/code&gt;), the backend follows strict routing rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adblock &amp;amp; Malware:&lt;/strong&gt; Automatically dropped (&lt;code&gt;blocked&lt;/code&gt;) at the server level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domestic/RU Domains &amp;amp; GeoIP:&lt;/strong&gt; Routed via &lt;strong&gt;Cloudflare WARP&lt;/strong&gt;. Ideally, your clients should use local routing rules (like &lt;code&gt;roscomvpn-routing&lt;/code&gt;) so domestic traffic never leaves their device. However, if a domestic packet &lt;em&gt;does&lt;/em&gt; leak to your VPS, WARP intercepts it and routes it out, masking your actual VPS server IP address from domestic logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming &amp;amp; Privacy:&lt;/strong&gt; Popular global services (like Disney+ or Reddit) are routed via &lt;strong&gt;Opera Proxy&lt;/strong&gt;, while &lt;code&gt;.onion&lt;/code&gt; addresses go directly through the &lt;strong&gt;Tor outbound network&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Note on Customization:&lt;/strong&gt; The outbound routing rules listed above (such as Opera Proxy, Tor, or specific domain rules) are configured &lt;strong&gt;as examples&lt;/strong&gt; to showcase the full capabilities of the script. You can easily modify, remove, or add your own custom routing parameters in the 3x-ui panel settings anytime after the installation is complete.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Prerequisites &amp;amp; Installation 🚀
&lt;/h2&gt;

&lt;p&gt;The deployment process requires minimal preparation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Requirements:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;A clean &lt;strong&gt;Ubuntu/Debian VPS&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A registered &lt;strong&gt;domain name&lt;/strong&gt; with an &lt;strong&gt;A-record&lt;/strong&gt; already pointed to your VPS IP address.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The 1-Command Deployment:
&lt;/h3&gt;

&lt;p&gt;Run the following command on your clean server root terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/AppsGanin/3xui-fast-install 3xui-personal
&lt;span class="nb"&gt;cd &lt;/span&gt;3xui-personal

&lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vpn.example.com bash deploy.sh 1.2.3.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What happens next?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The script detects and installs Docker/Compose if missing.&lt;/li&gt;
&lt;li&gt;It automatically generates unique, high-entropy credentials and secure ports for your 3x-ui panel dashboard.&lt;/li&gt;
&lt;li&gt;It configures your firewall, opens only the necessary ports (22, 80, 443, 63000 UDP), and locks down the rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once finished, the console will print your &lt;strong&gt;ready-to-use admin panel URL, username, and password&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Log in, create your client configs in the web UI, and you are good to go!&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion &amp;amp; Open Source
&lt;/h2&gt;

&lt;p&gt;This setup cuts down deployment time from an hour of manual configuration to literally 60 seconds, while maintaining best-practice security baselines out-of-the-box.&lt;/p&gt;

&lt;p&gt;The project is completely open-source. Feel free to review the bash script logic, submit issues, or contribute improvements.&lt;/p&gt;

&lt;p&gt;⭐️ &lt;strong&gt;Check out the repository on GitHub:&lt;/strong&gt; 👉 &lt;a href="https://github.com/AppsGanin/3xui-fast-install" rel="noopener noreferrer"&gt;https://github.com/AppsGanin/3xui-fast-install&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>privacy</category>
      <category>network</category>
    </item>
    <item>
      <title>Zero-Cost AI in VS Code</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Sat, 23 May 2026 17:02:39 +0000</pubDate>
      <link>https://dev.to/dmitryganin/zero-cost-ai-in-vs-code-2g24</link>
      <guid>https://dev.to/dmitryganin/zero-cost-ai-in-vs-code-2g24</guid>
      <description>&lt;h1&gt;
  
  
  Zero-Cost AI: Accessing Premium Models in VS Code Without API Keys
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;How I built a VS Code extension that gives you free access to Qwen-Max and DeepSeek models using only your existing web account — no billing, no tokens, no limits.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 The Problem with Modern AI Tools
&lt;/h2&gt;

&lt;p&gt;The state of AI development has become expensive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API keys&lt;/strong&gt; needed for every provider&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-token pricing&lt;/strong&gt; that adds up quickly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits&lt;/strong&gt; blocking your workflow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple subscriptions&lt;/strong&gt; to different services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Premium AI services charge significant monthly fees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT Plus&lt;/strong&gt;: $20/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Pro&lt;/strong&gt;: $20/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini Advanced&lt;/strong&gt;: $20/month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's over $600/year for basic access! And you still need separate accounts for each service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if there was another way?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 The Solution: Browser-Based Authentication
&lt;/h2&gt;

&lt;p&gt;I developed &lt;strong&gt;AI Free VSCode&lt;/strong&gt; — an open-source extension that leverages your existing &lt;strong&gt;free tier accounts&lt;/strong&gt; from AI providers through browser automation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Innovation
&lt;/h3&gt;

&lt;p&gt;Instead of requiring API keys (which often have strict rate limits), the extension:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Uses &lt;strong&gt;Playwright&lt;/strong&gt; to automate a real Chromium browser session&lt;/li&gt;
&lt;li&gt;Stores authentication cookies locally&lt;/li&gt;
&lt;li&gt;Makes requests through the official web APIs&lt;/li&gt;
&lt;li&gt;Gives you full access to the &lt;strong&gt;same free tier&lt;/strong&gt; available on their websites&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Result?
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;Zero cost&lt;/strong&gt; - uses existing free accounts&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;No API keys&lt;/strong&gt; - just sign in once&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Higher limits&lt;/strong&gt; - same as browsing the website&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Native integration&lt;/strong&gt; - works directly in Copilot Chat&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Agent mode&lt;/strong&gt; - full tool calling support&lt;/p&gt;


&lt;h2&gt;
  
  
  🏗 Architecture Overview
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Extension Structure
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ai-free-vscode/
├── src/
│   ├── extension.mjs          # Entry point &amp;amp; commands
│   ├── lmProvider.mjs         # Unified LM provider interface
│   ├── deepseek/
│   │   ├── auth.mjs           # Browser login with Playwright
│   │   ├── client.mjs         # API client implementation
│   │   ├── provider.mjs       # Model logic &amp;amp; session management
│   │   └── config.mjs         # Configuration constants
│   ├── qwen/
│   │   ├── auth.mjs           # Qwen authentication
│   │   ├── client.mjs         # Qwen API client
│   │   └── provider.mjs       # Qwen model implementation
│   ├── utils/
│   │   ├── logger.mjs         # Debug logging
│   │   ├── rateLimiter.mjs    # Rate limiting protection
│   │   ├── responseValidator.mjs
│   │   └── tokenValidator.mjs
│   └── promptUtils.mjs        # Message formatting
├── package.json               # Extension manifest
└── README.md                  # Documentation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Core Components
&lt;/h3&gt;
&lt;h4&gt;
  
  
  1. Authentication Flow
&lt;/h4&gt;

&lt;p&gt;The extension registers commands for users to authenticate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deepseek.login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;clearProfileSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Clear old session&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loginAndSaveAuth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// New login via Playwright&lt;/span&gt;
    &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookieHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookieHeader&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Process:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Opens Chromium browser via Playwright&lt;/li&gt;
&lt;li&gt;User signs into provider normally&lt;/li&gt;
&lt;li&gt;Session cookies captured and stored locally&lt;/li&gt;
&lt;li&gt;Cookies used for subsequent API requests&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Unified Provider Interface
&lt;/h4&gt;

&lt;p&gt;All models are unified under a single vendor namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AiFreeVscodeChatModelProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;provideLanguageModelChatResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Convert VS Code messages to API format&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;convertedMessages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;convertMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;convertToolSchemas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;messagesToPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;convertedMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Route to appropriate provider&lt;/span&gt;
    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;family&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deepseek&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;deepseekComplete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;qwen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;qwenComplete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;onText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;onThinking&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Process:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routes VS Code chat requests to appropriate provider&lt;/li&gt;
&lt;li&gt;Handles both DeepSeek and Qwen models&lt;/li&gt;
&lt;li&gt;Converts messages to API format&lt;/li&gt;
&lt;li&gt;Manages streaming responses&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. Smart Session Management
&lt;/h4&gt;

&lt;p&gt;Maintains conversation continuity with session caching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionIdCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runComplete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;threadKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;messagesCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Start fresh session for first message in thread&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messagesCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sessionIdCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;threadKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Try cached session first (for conversation continuity)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cachedSessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sessionIdCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;threadKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cachedSessionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cachedSessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Success!&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Retry with new session&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createSession&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;sessionIdCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;threadKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔧 Installation &amp;amp; Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Install the Extension
&lt;/h3&gt;

&lt;p&gt;Download the latest &lt;code&gt;.vsix&lt;/code&gt; file from &lt;a href="https://github.com/AppsGanin/ai-free-vscode/releases" rel="noopener noreferrer"&gt;Releases&lt;/a&gt; and install via VS Code Extensions panel.&lt;/p&gt;

&lt;p&gt;Or develop locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/AppsGanin/ai-free-vscode
&lt;span class="nb"&gt;cd &lt;/span&gt;ai-free-vscode
npm &lt;span class="nb"&gt;install&lt;/span&gt;  &lt;span class="c"&gt;# installs dependencies + Playwright Chromium&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;F5&lt;/code&gt; to launch in Extension Development Host.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Sign In to Provider
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open Command Palette (&lt;code&gt;Cmd+Shift+P&lt;/code&gt; / &lt;code&gt;Ctrl+Shift+P&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Run &lt;strong&gt;"AI Free VSCode: DeepSeek: Sign In (Playwright)"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A browser window opens automatically&lt;/li&gt;
&lt;li&gt;Log in to your account normally&lt;/li&gt;
&lt;li&gt;Window closes when session is saved&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repeat for &lt;strong&gt;Qwen&lt;/strong&gt; or other supported providers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Start Chatting
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Open Copilot Chat panel (⌘+L)&lt;/li&gt;
&lt;li&gt;Select your preferred model from dropdown&lt;/li&gt;
&lt;li&gt;Start asking questions!&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 Supported Models
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeepSeek V4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;deepseek-default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;General purpose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeepSeek V4 Expert&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;deepseek-expert&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Complex reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen2.5-Max&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen-max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Powerful tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen3.6-Plus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen-plus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Long documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen3-Max&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen3-max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Flagship quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen3-Coder&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen-coder&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Code generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen3.5-Flash&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fastest responses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All models support &lt;strong&gt;tool calling&lt;/strong&gt; for Agent mode operations like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File reading/writing&lt;/li&gt;
&lt;li&gt;Terminal execution&lt;/li&gt;
&lt;li&gt;Multi-step debugging&lt;/li&gt;
&lt;li&gt;Code refactoring&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛠 Technical Deep Dive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How Messages Are Processed
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Message Conversion
&lt;/h4&gt;

&lt;p&gt;VS Code messages are converted to API-compatible format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convertMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Handle text content&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;LanguageModelTextPart&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Extract tool calls from assistant&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolCalls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;LanguageModelToolCallPart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;}));&lt;/span&gt;

      &lt;span class="c1"&gt;// Generate separate "tool" messages for results&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;LanguageModelToolResultPart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}));&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;toolResults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;toolCalls&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Tool Call Detection
&lt;/h4&gt;

&lt;p&gt;The system detects markdown fences indicating tool calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOOL_FENCES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;`tool_call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;tool_call&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_call&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findFence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fence&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;TOOL_FENCES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fence&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Stream processing&lt;/span&gt;
&lt;span class="nx"&gt;streamBuf&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;findFence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;streamBuf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Emit text before fence, suppress tool call block&lt;/span&gt;
  &lt;span class="nf"&gt;flushStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;streamBuf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;streamBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;inToolCall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents raw tool call blocks from appearing in the chat UI while still executing them properly.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Thinking Mode Support
&lt;/h4&gt;

&lt;p&gt;For models with explicit reasoning phases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;thinkingStarted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;thinkingText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onThinking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;thinkingText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;thinkingStarted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// When content starts, emit thinking as collapsible block&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;thinkingStarted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LanguageModelThinkingPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;thinkingText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;thinking-0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VS Code displays this as a native collapsible &lt;strong&gt;"💭 Thinking"&lt;/strong&gt; section above responses.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔐 Security &amp;amp; Privacy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Happens to Your Data?
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;Cookies stored locally&lt;/strong&gt; - Only your machine, encrypted by OS&lt;br&gt;
✅ &lt;strong&gt;No cloud storage&lt;/strong&gt; - We never transmit your credentials&lt;br&gt;
✅ &lt;strong&gt;Session isolation&lt;/strong&gt; - Each provider maintains separate sessions&lt;br&gt;
✅ &lt;strong&gt;No telemetry&lt;/strong&gt; - No usage statistics sent anywhere&lt;/p&gt;
&lt;h3&gt;
  
  
  Error Handling
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Graceful handling of various errors&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isNotSignedIn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showErrorMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please sign in first&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isAuthError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Cookie expired - force re-login&lt;/span&gt;
    &lt;span class="nf"&gt;clearProfileSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isBizError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Business logic error with formatted message&lt;/span&gt;
    &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;formatBizError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bizCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bizMsg&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  ⚠️ Limitations &amp;amp; Caveats
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Important Considerations
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Terms of Service&lt;/strong&gt; - Automating browser sessions may violate provider ToS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account Risk&lt;/strong&gt; - Your account could be restricted (use at your own risk)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stability&lt;/strong&gt; - Providers can change APIs without notice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single Session&lt;/strong&gt; - Only one active user session at a time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Enterprise Support&lt;/strong&gt; - Not suitable for corporate compliance requirements&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Mitigation Strategies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use separate accounts from main email&lt;/li&gt;
&lt;li&gt;Don't abuse the service (reasonable usage only)&lt;/li&gt;
&lt;li&gt;Keep extension updated for API changes&lt;/li&gt;
&lt;li&gt;Maintain backups of important code/settings&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  🚀 Real-World Use Cases
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Scenario 1: Code Review Assistant
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Ask about potential bugs
&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review this Python function for memory leaks:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;pastes&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;Assistant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Analyzes&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="n"&gt;structure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;identifies&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;leaks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;suggests&lt;/span&gt; &lt;span class="n"&gt;fixes&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;explanations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Scenario 2: Database Query Optimization
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Paste slow query&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;Assistant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Suggests&lt;/span&gt; &lt;span class="n"&gt;indexing&lt;/span&gt; &lt;span class="n"&gt;strategies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="n"&gt;rewriting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;alternative&lt;/span&gt; &lt;span class="n"&gt;approaches&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Scenario 3: Full Stack Debugging
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Identify error in terminal&lt;/li&gt;
&lt;li&gt;Ask assistant to analyze stack trace&lt;/li&gt;
&lt;li&gt;Get root cause explanation&lt;/li&gt;
&lt;li&gt;Receive fix suggestion with code example&lt;/li&gt;
&lt;li&gt;Apply fix directly in editor&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  🤝 Contributing
&lt;/h2&gt;

&lt;p&gt;This is an open-source hobby project built by enthusiasts, for enthusiasts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ways to contribute:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fix bugs&lt;/strong&gt; - See &lt;a href="https://github.com/AppsGanin/ai-free-vscode/issues" rel="noopener noreferrer"&gt;open issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add new models&lt;/strong&gt; - Implement additional AI providers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improve docs&lt;/strong&gt; - Clarify setup instructions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enhance UX&lt;/strong&gt; - Better error messages, UI improvements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write tests&lt;/strong&gt; - Increase coverage for edge cases&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Getting started:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/AppsGanin/ai-free-vscode
&lt;span class="nb"&gt;cd &lt;/span&gt;ai-free-vscode
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="c"&gt;# Edit code, press F5 to test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contributions welcome! PRs are always appreciated.&lt;/p&gt;




&lt;h2&gt;
  
  
  📝 Legal Disclaimer
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;This extension is unofficial and not affiliated with any AI provider.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use at your own risk&lt;/strong&gt; - Automating web sessions may violate ToS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No guarantees&lt;/strong&gt; - May stop working if providers change APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No liability&lt;/strong&gt; - Authors not responsible for consequences&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always review Terms of Service before use.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🎯 Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AI Free VSCode&lt;/strong&gt; demonstrates that you don't need expensive API keys or multiple subscriptions to access premium AI capabilities. By leveraging browser automation and existing free tiers, we've created a solution that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💰 &lt;strong&gt;Costs nothing&lt;/strong&gt; - literally $0 monthly subscription&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Works instantly&lt;/strong&gt; - one-time sign-in, perpetual access&lt;/li&gt;
&lt;li&gt;🔒 &lt;strong&gt;Respects privacy&lt;/strong&gt; - all data stays local&lt;/li&gt;
&lt;li&gt;🛠️ &lt;strong&gt;Integrates seamlessly&lt;/strong&gt; - native VS Code experience&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you're a student learning to code, a indie developer building your startup, or just someone who wants powerful AI tools without breaking the bank - this extension removes financial barriers and puts cutting-edge technology in your hands.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Ready to try it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/AppsGanin/ai-free-vscode/releases" rel="noopener noreferrer"&gt;Download the extension&lt;/a&gt;&lt;br&gt;&lt;br&gt;
⭐ Star the repo if it helps your workflow&lt;br&gt;&lt;br&gt;
📣 Share with fellow developers&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let's democratize AI access together!&lt;/strong&gt; 🚀&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
