Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/build-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Build Check

on:
pull_request:
workflow_dispatch:

jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'

- name: Build
run: dotnet build AgentsPanelExtension.sln --configuration Debug -p:Platform=x64
41 changes: 41 additions & 0 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Deploy Pages

on:
push:
branches: [main]
paths:
- 'docs/**'
- '.github/workflows/deploy-pages.yml'
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

# Allow one concurrent deployment; let an in-progress run finish.
concurrency:
group: pages
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Configure Pages
uses: actions/configure-pages@v5

- name: Upload docs/ as Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
162 changes: 162 additions & 0 deletions .github/workflows/release-msix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
name: Build and Release MSIX

on:
workflow_dispatch:
inputs:
version:
description: 'Version number (leave empty to auto-detect from csproj)'
required: false
type: string
release_notes:
description: 'What is new in this version'
required: false
default: 'Updates happened.'
type: string

env:
DISPLAY_NAME: Agents Panel
EXTENSION_NAME: AgentsPanelExtension
FOLDER_NAME: AgentsPanelExtension

jobs:
build:
runs-on: windows-2022
permissions:
contents: write
actions: read

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup .NET 9
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'

- name: Get version from project
id: version
run: |
if ("${{ github.event.inputs.version }}" -ne "") {
$version = "${{ github.event.inputs.version }}"
} else {
$projectFile = "${{ env.FOLDER_NAME }}/${{ env.EXTENSION_NAME }}.csproj"
$xml = [xml](Get-Content $projectFile)
$version = $xml.Project.PropertyGroup.AppxPackageVersion | Select-Object -First 1
if (-not $version) { throw "Version not found in project file" }
}
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
Write-Host "Using version: $version"
shell: pwsh

- name: Build x64 MSIX
run: |
Set-Location "${{ env.FOLDER_NAME }}"
dotnet build --configuration Release `
-p:GenerateAppxPackageOnBuild=true `
-p:Platform=x64 `
-p:RuntimeIdentifier=win-x64 `
-p:AppxPackageDir="AppPackages\x64\" `
-p:PublishTrimmed=false
shell: pwsh

- name: Build ARM64 MSIX
run: |
Set-Location "${{ env.FOLDER_NAME }}"
dotnet build --configuration Release `
-p:GenerateAppxPackageOnBuild=true `
-p:Platform=ARM64 `
-p:RuntimeIdentifier=win-arm64 `
-p:AppxPackageDir="AppPackages\ARM64\" `
-p:PublishTrimmed=false
shell: pwsh

- name: Find MSIX files
id: msix
run: |
Set-Location "${{ env.FOLDER_NAME }}"
Write-Host "All MSIX files found:"
Get-ChildItem "." -Recurse -Filter "*.msix" | ForEach-Object { Write-Host $_.FullName }
$x64 = Get-ChildItem "." -Recurse -Filter "*.msix" | Where-Object { $_.Name -match "x64" } | Select-Object -First 1
$arm64 = Get-ChildItem "." -Recurse -Filter "*.msix" | Where-Object { $_.Name -match "arm64" } | Select-Object -First 1
if (-not $x64) { throw "x64 MSIX not found" }
if (-not $arm64) { throw "ARM64 MSIX not found" }
echo "X64_PATH=$($x64.FullName)" >> $env:GITHUB_OUTPUT
echo "ARM64_PATH=$($arm64.FullName)" >> $env:GITHUB_OUTPUT
Write-Host "x64: $($x64.FullName)"
Write-Host "arm64: $($arm64.FullName)"
shell: pwsh

- name: Create bundle mapping
run: |
Set-Location "${{ env.FOLDER_NAME }}"
$x64Rel = (Resolve-Path -Relative "${{ steps.msix.outputs.X64_PATH }}")
$arm64Rel = (Resolve-Path -Relative "${{ steps.msix.outputs.ARM64_PATH }}")
$bundleName = "${{ env.EXTENSION_NAME }}_${{ steps.version.outputs.VERSION }}_x64.msix"
$bundleArm = "${{ env.EXTENSION_NAME }}_${{ steps.version.outputs.VERSION }}_arm64.msix"
$content = "[Files]`n`"$x64Rel`" `"$bundleName`"`n`"$arm64Rel`" `"$bundleArm`""
$content | Set-Content bundle_mapping.txt
Get-Content bundle_mapping.txt
shell: pwsh

- name: Find makeappx
id: makeappx
run: |
$arch = switch ($env:PROCESSOR_ARCHITECTURE) { "AMD64" { "x64" } "ARM64" { "arm64" } default { "x64" } }
$found = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\$arch\makeappx.exe" -ErrorAction SilentlyContinue `
| Sort-Object FullName -Descending | Select-Object -First 1
if (-not $found) { throw "makeappx.exe not found" }
echo "PATH=$($found.FullName)" >> $env:GITHUB_OUTPUT
Write-Host "makeappx: $($found.FullName)"
shell: pwsh

- name: Bundle x64 + ARM64 into msixbundle
run: |
Set-Location "${{ env.FOLDER_NAME }}"
$bundle = "${{ env.EXTENSION_NAME }}_${{ steps.version.outputs.VERSION }}_Bundle.msixbundle"
& "${{ steps.makeappx.outputs.PATH }}" bundle /v /f bundle_mapping.txt /p $bundle
echo "BUNDLE_PATH=${{ env.FOLDER_NAME }}\$bundle" >> $env:GITHUB_ENV
shell: pwsh

- name: Decode and import signing certificate
run: |
$pfxBytes = [Convert]::FromBase64String("${{ secrets.SIGNING_CERT_PFX }}")
$pfxPath = "$env:RUNNER_TEMP\signing.pfx"
[IO.File]::WriteAllBytes($pfxPath, $pfxBytes)
echo "PFX_PATH=$pfxPath" >> $env:GITHUB_ENV
shell: pwsh

- name: Sign msixbundle
run: |
$signtool = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe" -ErrorAction SilentlyContinue `
| Sort-Object Name -Descending | Select-Object -First 1
if (-not $signtool) { throw "signtool.exe not found" }
& $signtool sign /fd SHA256 /p "${{ secrets.SIGNING_CERT_PASSWORD }}" /f "$env:PFX_PATH" "$env:BUNDLE_PATH"
shell: pwsh

- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ steps.version.outputs.VERSION }}
name: "${{ env.DISPLAY_NAME }} v${{ steps.version.outputs.VERSION }}"
body: |
## ${{ env.DISPLAY_NAME }} v${{ steps.version.outputs.VERSION }}

## What's New
${{ github.event.inputs.release_notes }}

## Installation

Download and double-click `${{ env.EXTENSION_NAME }}_${{ steps.version.outputs.VERSION }}_Bundle.msixbundle`.

> **Note:** The package is self-signed. If Windows blocks installation, right-click the `.msixbundle` → Properties → Digital Signatures → install the certificate to "Trusted People" first.

Alternatively, install via PowerShell:
```powershell
Add-AppxPackage .\${{ env.EXTENSION_NAME }}_${{ steps.version.outputs.VERSION }}_Bundle.msixbundle
```
files: ${{ env.FOLDER_NAME }}/${{ env.EXTENSION_NAME }}_${{ steps.version.outputs.VERSION }}_Bundle.msixbundle
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
8 changes: 7 additions & 1 deletion AgentsPanelExtension/AgentsPanelExtension.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
<AppxPackageIdentityName>CostaFotiadis.AgentsPanelforCommandPalette</AppxPackageIdentityName>
<AppxPackagePublisher>CN=3D57AA92-97A9-42D2-8CB0-4207D9145514</AppxPackagePublisher>
<AppxPackageVersion>0.1.0.0</AppxPackageVersion>
<!-- Assembly version mirrors the package version so Helpers/AppInfo.cs can derive the HTTP
User-Agent from the assembly — bump AppxPackageVersion and this follows. -->
<Version>$(AppxPackageVersion)</Version>
<Company>Costa Fotiadis</Company>
<Product>Agents Panel for Command Palette</Product>
<Copyright>© Costa Fotiadis</Copyright>
<EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>

Expand Down Expand Up @@ -50,7 +56,7 @@
<Copy SourceFiles="$(MSBuildProjectDirectory)\Assets\Wide310x150Logo.scale-200.png" DestinationFiles="$(MSBuildProjectDirectory)\Assets\Wide310x150Logo.png" SkipUnchangedFiles="true" />
<Copy SourceFiles="$(MSBuildProjectDirectory)\Assets\SplashScreen.scale-200.png" DestinationFiles="$(MSBuildProjectDirectory)\Assets\SplashScreen.png" SkipUnchangedFiles="true" />
<Copy SourceFiles="$(MSBuildProjectDirectory)\Assets\Square150x150Logo.scale-200.png" DestinationFiles="$(MSBuildProjectDirectory)\Assets\LargeTile.png" SkipUnchangedFiles="true" />
<!-- StoreLogo.png already exists at the correct path, no copy needed -->
<!-- StoreLogo has no base-name file — only scale variants exist; MRT resolves them, so no copy -->
</Target>

<ItemGroup>
Expand Down
5 changes: 1 addition & 4 deletions AgentsPanelExtension/Data/Claude/ClaudeUsageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@
// The beta header the endpoint requires alongside a consumer OAuth token.
private const string AnthropicBeta = "oauth-2025-04-20";

// Honest self-identification (deliberate decision: no client spoofing). One place to bump.
private const string UserAgent = "agents-panel/0.1.0";

// One client for the process. Headers that never change ride on it; the Authorization header is
// per-request because the token is re-read from disk on every fetch.
private static readonly HttpClient Http = CreateClient();
Expand Down Expand Up @@ -117,12 +114,12 @@
private static HttpClient CreateClient()
{
var client = new HttpClient();
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", UserAgent);
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", AppInfo.UserAgent);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return client;
}

private static IReadOnlyList<DomainUsageWindow> MapWindows(ApiClaudeUsageDto dto)

Check warning on line 122 in AgentsPanelExtension/Data/Claude/ClaudeUsageProvider.cs

View workflow job for this annotation

GitHub Actions / build

Change return type of method 'MapWindows' from 'System.Collections.Generic.IReadOnlyList<AgentsPanelExtension.DomainUsageWindow>' to 'System.Collections.Generic.List<AgentsPanelExtension.DomainUsageWindow>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)

Check warning on line 122 in AgentsPanelExtension/Data/Claude/ClaudeUsageProvider.cs

View workflow job for this annotation

GitHub Actions / build

Change return type of method 'MapWindows' from 'System.Collections.Generic.IReadOnlyList<AgentsPanelExtension.DomainUsageWindow>' to 'System.Collections.Generic.List<AgentsPanelExtension.DomainUsageWindow>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)
{
var windows = new List<DomainUsageWindow>(5);

Expand Down
5 changes: 1 addition & 4 deletions AgentsPanelExtension/Data/Codex/CodexUsageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@
// style is for API-key auth, which this provider doesn't use.
private const string UsageEndpoint = "https://chatgpt.com/backend-api/wham/usage";

// Honest self-identification (deliberate decision: no client spoofing). One place to bump.
private const string UserAgent = "agents-panel/0.1.0";

// One client for the process. Headers that never change ride on it; the Authorization and
// account-id headers are per-request because credentials are re-read from disk on every fetch.
private static readonly HttpClient Http = CreateClient();
Expand Down Expand Up @@ -122,12 +119,12 @@
private static HttpClient CreateClient()
{
var client = new HttpClient();
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", UserAgent);
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", AppInfo.UserAgent);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return client;
}

private static IReadOnlyList<DomainUsageWindow> MapWindows(ApiCodexUsageDto dto, DateTimeOffset now)

Check warning on line 127 in AgentsPanelExtension/Data/Codex/CodexUsageProvider.cs

View workflow job for this annotation

GitHub Actions / build

Change return type of method 'MapWindows' from 'System.Collections.Generic.IReadOnlyList<AgentsPanelExtension.DomainUsageWindow>' to 'System.Collections.Generic.List<AgentsPanelExtension.DomainUsageWindow>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)

Check warning on line 127 in AgentsPanelExtension/Data/Codex/CodexUsageProvider.cs

View workflow job for this annotation

GitHub Actions / build

Change return type of method 'MapWindows' from 'System.Collections.Generic.IReadOnlyList<AgentsPanelExtension.DomainUsageWindow>' to 'System.Collections.Generic.List<AgentsPanelExtension.DomainUsageWindow>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)
{
var windows = new List<DomainUsageWindow>(3);

Expand Down
7 changes: 3 additions & 4 deletions AgentsPanelExtension/Data/Copilot/CopilotUsageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@

private const string UsageEndpoint = "https://api.github.com/copilot_internal/user";

// Honest self-identification (deliberate decision: no client spoofing — no Editor-Version
// masquerade either; the endpoint answers plain user agents). One place to bump.
private const string UserAgent = "agents-panel/0.1.0";
// Deliberate decision for this endpoint: no Editor-Version masquerade either — it answers
// plain user agents (AppInfo.UserAgent).

// One client for the process. Headers that never change ride on it; Authorization is
// per-request because the token is re-discovered on every fetch.
Expand Down Expand Up @@ -112,7 +111,7 @@
private static HttpClient CreateClient()
{
var client = new HttpClient();
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", UserAgent);
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", AppInfo.UserAgent);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return client;
}
Expand All @@ -122,7 +121,7 @@
// button label inside its ~15-char budget; "Code" = code completions). premium_interactions
// stays unqualified: on the plans where it's metered it's the ONLY window, so plain "Mo" reads
// best.
private static IReadOnlyList<DomainUsageWindow> MapWindows(ApiCopilotUserDto dto)

Check warning on line 124 in AgentsPanelExtension/Data/Copilot/CopilotUsageProvider.cs

View workflow job for this annotation

GitHub Actions / build

Change return type of method 'MapWindows' from 'System.Collections.Generic.IReadOnlyList<AgentsPanelExtension.DomainUsageWindow>' to 'System.Collections.Generic.List<AgentsPanelExtension.DomainUsageWindow>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)

Check warning on line 124 in AgentsPanelExtension/Data/Copilot/CopilotUsageProvider.cs

View workflow job for this annotation

GitHub Actions / build

Change return type of method 'MapWindows' from 'System.Collections.Generic.IReadOnlyList<AgentsPanelExtension.DomainUsageWindow>' to 'System.Collections.Generic.List<AgentsPanelExtension.DomainUsageWindow>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)
{
var windows = new List<DomainUsageWindow>(3);
var resetsAt = ParseResetDate(dto.QuotaResetDateUtc) ?? ParseResetDate(dto.QuotaResetDate);
Expand Down
16 changes: 16 additions & 0 deletions AgentsPanelExtension/Helpers/AppInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;

namespace AgentsPanelExtension;

// The one honest self-identification string every provider HTTP client sends (deliberate decision:
// no client spoofing, no Editor-Version masquerades). The version rides in from the assembly —
// csproj <Version> mirrors <AppxPackageVersion>, so a release bumps the csproj once and the UA
// follows automatically. Never hardcode a version literal here or in a provider.
internal static class AppInfo
{
// Three-part ("agents-panel/0.1.0"), matching the historical literal the endpoints have seen.
public static readonly string UserAgent =
typeof(AppInfo).Assembly.GetName().Version is { } v
? $"agents-panel/{v.Major}.{v.Minor}.{v.Build}"
: "agents-panel";
}
6 changes: 4 additions & 2 deletions AgentsPanelExtension/Helpers/UsageStatusHint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ internal static class UsageStatusHint
}

// Status rows are informational — there is nothing useful for Enter to do (we can't sign the user
// in), so a NoOpCommand with a stable non-empty Id. Subtitle and pill come from the per-status
// helpers above so the text/colors stay defined once.
// in), so a NoOpCommand with a stable non-empty Id. Decision (2026-08-14): keep it that way — no
// deep-links, no third-party sign-in guidance; the row states the fact and the user handles their
// own agent apps. Subtitle and pill come from the per-status helpers above so the text/colors stay
// defined once.
private static ListItem Row(DomainUsageSnapshot snapshot, string title, string kind) =>
new(new NoOpCommand { Id = $"com.costafotiadis.agentspanel.status.{snapshot.ProviderId}.{kind}" })
{
Expand Down
47 changes: 1 addition & 46 deletions AgentsPanelExtension/Pages/LegalPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,7 @@ namespace AgentsPanelExtension;
// providers, no lifecycle. (Modeled on MarketExtension's DataSourcesPage.)
internal sealed partial class LegalPage : ContentPage
{
private readonly MarkdownContent _content = new(
"""
# The Legal Bit

This app is an **independent, third-party tool**. It is **not
affiliated with, endorsed by, or connected to** Anthropic, Claude,
OpenAI, ChatGPT, GitHub, Copilot, or any AI provider. Provider names
and logos belong to their respective owners.

## No backend, no tracking

This app has **no server, no analytics, and no telemetry**. It
collects nothing, stores nothing outside your machine, and shares
nothing with anyone. The only network requests it makes are the usage
checks you see on screen, sent **directly to the provider**.

## Your credentials stay yours

The app reads the sign-in credentials that a provider's own app
(e.g. Claude Code) already keeps on your machine. It never creates,
modifies, uploads, or proxies them — each token is read locally and
sent **only to the provider it belongs to**, and nowhere else.

Your account and any agreement governing it are **between you and
that provider**. This app is just a surface that displays what the
provider reports, and you are responsible for using your account
within the provider's terms.

## No guarantees

Usage numbers come from provider endpoints that are **not officially
documented for third-party use** — they can be delayed, inaccurate,
or stop working at any time without notice. The app is provided
**as is, without warranty of any kind**. Don't rely on it as the
authoritative record of your usage or billing — the provider's own
app and website are.

## What this app does and doesn't do

**Does:** read your local sign-in, ask the provider for your usage
numbers, and show them to you.

**Doesn't:** run a server, track you, phone home, refresh or modify
your credentials, or send anything anywhere except the provider's
own API.
""");
private readonly MarkdownContent _content = new(Resources.Legal_Markdown);

public LegalPage()
{
Expand Down
Loading
Loading