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
56 changes: 41 additions & 15 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
<p align="center">
Mission control for AI-assisted development on Windows: GitHub issues, isolated git worktrees and embedded Claude Code terminals in one app.
<br />
<a href="https://github.com/jellebuning/kontext/issues">Report Bug</a>
<a href="https://github.com/buning-software/kontext/issues">Report Bug</a>
·
<a href="https://github.com/jellebuning/kontext/issues">Request Feature</a>
<a href="https://github.com/buning-software/kontext/issues">Request Feature</a>
</p>
</div>

Expand All @@ -35,6 +35,7 @@
<li><a href="#requirements">Requirements</a></li>
<li><a href="#build-and-run">Build and run</a></li>
<li><a href="#test">Test</a></li>
<li><a href="#release">Release</a></li>
</ul>
</li>
<li><a href="#contributing">Contributing</a></li>
Expand All @@ -54,7 +55,7 @@ Kontext lets you work on all your repositories' issues at once and leave Claude
* **One worktree per issue.** Selecting an issue clones the repository when needed, creates a `features/{number}-{title}` branch in its own worktree, and opens an empty Claude Code session there. Nothing is asked of you, and nothing is shared between issues.
* **Know when Claude needs you.** Claude Code hooks report whether a session is working or waiting for input; the circle in the sidebar changes accordingly.
* **Resume where you left off.** Sessions are remembered per issue and continued with `claude --resume` after a restart.
* **Open it anywhere.** Open a worktree in File Explorer, Windows Terminal or Visual Studio from the issue's header.
* **Open it anywhere.** Open a worktree in File Explorer, Windows Terminal or its solution file (`.slnx` or `.sln`, in whatever app is registered for it) from the issue's header.
* **Clean up when done.** For a closed issue, Clear session removes the worktree and its branch.
* **Modern Architecture:** Built with SOLID principles and design patterns for maximum maintainability: a modular monolith with compiler-enforced module boundaries.
* **Native Windows look.** WinUI 3 with Mica, light and dark themes, and a terminal with zoom and mouse support.
Expand All @@ -81,7 +82,11 @@ Before beginning, ensure that your development environment is properly configure
* [.NET 10 SDK](https://dotnet.microsoft.com/download)
* [Git for Windows](https://git-scm.com/download/win) on your `PATH`
* The [Claude Code](https://docs.claude.com/en/docs/claude-code) CLI (`claude`) on your `PATH`
* A GitHub personal access token with the `repo` scope; Kontext asks for it on first start and stores it in Windows Credential Locker
* A GitHub account; you sign in with it on first start (GitHub device flow) and Kontext stores the resulting token in Windows Credential Locker

### Configuration

Sign-in needs a GitHub OAuth App with Device Flow enabled. For local development, copy `src/Kontext/appsettings.local.example.json` to `src/Kontext/appsettings.local.json` (git-ignored) and set `GitHub:ClientId` to the app's client id (the client id is not a secret). CI writes the repository variable `KONTEXT_GITHUB_CLIENT_ID` into `appsettings.json` before building. Organisations that restrict OAuth apps must approve the app before their repositories and issues become available.

### Build and run

Expand All @@ -98,7 +103,28 @@ Stop a running instance before building again, because it locks the build output
dotnet test Kontext.slnx -c Release
```

Warnings are treated as errors. The tests include integration tests that run real `git` against a temporary repository.
Warnings are treated as errors. Tests are split by kind, one project per kind and module:

* `Kontext.*.UnitTests` are fast and in-memory: no filesystem, processes or network.
* `Kontext.*.IntegrationTests` touch the real world: the filesystem, a loopback HTTP server, or real `git` against a temporary repository.
* `Kontext.ArchitectureTests` and `Kontext.CompositionTests` check the module boundaries and the dependency injection wiring.

To run only the unit tests:

```powershell
Get-ChildItem tests -Recurse -Filter *.UnitTests.csproj | ForEach-Object { dotnet test $_.FullName -c Release }
```

### Release

Publishing a GitHub release tagged like `v1.2.3` runs the tests, then builds a signed, self-contained x64 MSIX and attaches it to the release together with `Kontext.cer`, the public certificate. The tag sets the package version. ARM64 is not built because the embedded terminal control only ships x64 binaries.

One-time setup:

* Signing uses the `CERTIFICATE_BASE64` (the `.pfx`, base64 encoded) and `CERTIFICATE_PASSWORD` secrets. They are organization secrets of `buning-software`, shared with the other Buning Software apps, and this repository has to be allowed to use them (organization Settings, Secrets and variables, Actions). The certificate subject must equal `Publisher` in `src/Kontext/Package.appxmanifest` (`CN=BuningSoftware`). To create a new certificate, for example when it expires, use `generate_certificate.ps1` in the [Surveil repository](https://github.com/buning-software/surveil), update both organization secrets, and never commit the `.pfx`.
* Set the repository variable `KONTEXT_GITHUB_CLIENT_ID`. An installed MSIX is read-only, so the client id has to be built in; the release fails without it.

To install a release, first trust `Kontext.cer` once (open it, choose Install Certificate, Local Machine, Trusted People), then open the `.msix`. Kontext still keeps its data under `%LOCALAPPDATA%\Kontext`: the package turns off file redirection so your clones and worktrees stay visible to Explorer, terminals and IDEs.

<!-- CONTRIBUTING -->
## Contributing
Expand Down Expand Up @@ -128,13 +154,13 @@ Distributed under the GNU Affero General Public License v3.0 License. See `LICEN

<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[contributors-shield]: https://img.shields.io/github/contributors/jellebuning/kontext.svg?style=for-the-badge
[contributors-url]: https://github.com/jellebuning/kontext/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/jellebuning/kontext.svg?style=for-the-badge
[forks-url]: https://github.com/jellebuning/kontext/network/members
[stars-shield]: https://img.shields.io/github/stars/jellebuning/kontext.svg?style=for-the-badge
[stars-url]: https://github.com/jellebuning/kontext/stargazers
[issues-shield]: https://img.shields.io/github/issues/jellebuning/kontext.svg?style=for-the-badge
[issues-url]: https://github.com/jellebuning/kontext/issues
[license-shield]: https://img.shields.io/github/license/jellebuning/kontext.svg?style=for-the-badge
[license-url]: https://github.com/jellebuning/kontext/blob/main/LICENSE
[contributors-shield]: https://img.shields.io/github/contributors/buning-software/kontext.svg?style=for-the-badge
[contributors-url]: https://github.com/buning-software/kontext/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/buning-software/kontext.svg?style=for-the-badge
[forks-url]: https://github.com/buning-software/kontext/network/members
[stars-shield]: https://img.shields.io/github/stars/buning-software/kontext.svg?style=for-the-badge
[stars-url]: https://github.com/buning-software/kontext/stargazers
[issues-shield]: https://img.shields.io/github/issues/buning-software/kontext.svg?style=for-the-badge
[issues-url]: https://github.com/buning-software/kontext/issues
[license-shield]: https://img.shields.io/github/license/buning-software/kontext.svg?style=for-the-badge
[license-url]: https://github.com/buning-software/kontext/blob/main/LICENSE
11 changes: 11 additions & 0 deletions .github/workflows/build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ jobs:
- name: Install dependencies
run: dotnet restore

- name: Set GitHub OAuth client id
if: ${{ vars.KONTEXT_GITHUB_CLIENT_ID != '' }}
shell: pwsh
env:
CLIENT_ID: ${{ vars.KONTEXT_GITHUB_CLIENT_ID }}
run: |
$path = 'src/Kontext/appsettings.json'
$settings = Get-Content $path -Raw | ConvertFrom-Json
$settings.GitHub.ClientId = $env:CLIENT_ID
$settings | ConvertTo-Json -Depth 5 | Set-Content $path

- name: Build
run: dotnet build --no-restore --configuration Release

Expand Down
143 changes: 143 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: Release

on:
release:
types: [published]

permissions:
contents: write

jobs:
test:
uses: ./.github/workflows/build-and-test.yaml

release:
needs: test
runs-on: windows-latest
strategy:
matrix:
# ARM64 is not built yet: EasyWindowsTerminalControl.WinUI only ships x64 binaries.
platform: [x64]
include:
- platform: x64
rid: win-x64

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Extract version from tag
id: version
shell: pwsh
env:
TAG: ${{ github.ref_name }}
run: |
$displayVersion = $env:TAG.TrimStart('v', 'V')
if ($displayVersion -notmatch '^\d+\.\d+\.\d+$') {
Write-Error "Release tag '$env:TAG' must look like v1.2.3 (got '$displayVersion' after stripping the prefix)."
exit 1
}
$manifestVersion = $displayVersion + '.0'
"DISPLAY_VERSION=$displayVersion" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"VERSION=$manifestVersion" | Out-File -FilePath $env:GITHUB_OUTPUT -Append

- name: Update Package.appxmanifest version
shell: pwsh
env:
VERSION: ${{ steps.version.outputs.VERSION }}
run: |
# Only the package identity carries the app version; MinVersion and MaxVersionTested must stay as they are.
$manifest = "src\Kontext\Package.appxmanifest"
$pattern = '(<Identity\b[^>]*?\sVersion=")\d+\.\d+\.\d+\.\d+(")'
$content = Get-Content $manifest -Raw
if ($content -notmatch $pattern) {
Write-Error "Could not find the <Identity> version in $manifest."
exit 1
}
[regex]::Replace($content, $pattern, "`${1}$env:VERSION`${2}") | Set-Content $manifest -NoNewline

- name: Setup .NET SDK
uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'

- name: Load nuget cache
uses: actions/cache@v6
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props') }}
restore-keys: |
${{ runner.os }}-nuget-

- name: Restore dependencies
run: dotnet restore

- name: Set GitHub OAuth client id
shell: pwsh
env:
CLIENT_ID: ${{ vars.KONTEXT_GITHUB_CLIENT_ID }}
run: |
# An installed MSIX is read-only, so users cannot add appsettings.local.json: the id must be built in.
if ([string]::IsNullOrWhiteSpace($env:CLIENT_ID)) {
Write-Error "Set the KONTEXT_GITHUB_CLIENT_ID repository variable; without it the released app cannot sign in."
exit 1
}
$path = 'src/Kontext/appsettings.json'
$settings = Get-Content $path -Raw | ConvertFrom-Json
$settings.GitHub.ClientId = $env:CLIENT_ID
$settings | ConvertTo-Json -Depth 5 | Set-Content $path

- name: Decode signing certificate
shell: pwsh
env:
CERTIFICATE_BASE64: ${{ secrets.CERTIFICATE_BASE64 }}
run: |
$bytes = [Convert]::FromBase64String($env:CERTIFICATE_BASE64)
[IO.File]::WriteAllBytes("${{ github.workspace }}\cert.pfx", $bytes)

- name: Build and Package MSIX
shell: pwsh
env:
CERT_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
run: |
dotnet publish src\Kontext\Kontext.csproj `
-c Release `
-r ${{ matrix.rid }} `
-p:Platform=${{ matrix.platform }} `
-p:WindowsPackageType=MSIX `
-p:GenerateAppxPackageOnBuild=true `
-p:AppxPackageDir="${{ github.workspace }}\AppxPackages\" `
-p:AppxPackageSigningEnabled=true `
-p:PackageCertificateKeyFile="${{ github.workspace }}\cert.pfx" `
-p:PackageCertificatePassword=$env:CERT_PASSWORD `
-p:AppxBundle=Never

- name: Export public certificate
if: matrix.platform == 'x64'
shell: pwsh
env:
CERT_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
run: |
$securePassword = ConvertTo-SecureString -String $env:CERT_PASSWORD -AsPlainText -Force
$cert = Get-PfxCertificate -FilePath "${{ github.workspace }}\cert.pfx" -Password $securePassword
Export-Certificate -Cert $cert -FilePath "${{ github.workspace }}\Kontext.cer" | Out-Null

- name: Remove signing certificate
if: always()
shell: pwsh
run: Remove-Item -Path "${{ github.workspace }}\cert.pfx" -Force -ErrorAction SilentlyContinue

- name: Rename MSIX with platform suffix
shell: pwsh
run: |
$package = Get-ChildItem -Path "${{ github.workspace }}\AppxPackages" -Filter "*.msix" -Recurse |
Select-Object -First 1
$newName = Join-Path $package.DirectoryName "Kontext_${{ steps.version.outputs.DISPLAY_VERSION }}_${{ matrix.platform }}.msix"
Rename-Item -Path $package.FullName -NewName $newName

- name: Upload to GitHub Release
uses: softprops/action-gh-release@v3
with:
files: |
AppxPackages/**/*.msix
*.cer
15 changes: 14 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,17 @@ nunit-*.xml
*.env.staging

*.idea
.vs/
.vs/
# Local developer settings (GitHub OAuth client id)
appsettings.local.json

# MSIX app packages
**/AppPackages/
AppxPackages/
*.msix
*.msixbundle
*.appx
*.appxbundle

# Code signing certificates
*.pfx
17 changes: 10 additions & 7 deletions Kontext.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@
</Folder>
<Folder Name="/tests/">
<Project Path="tests/Kontext.ArchitectureTests/Kontext.ArchitectureTests.csproj" />
<Project Path="tests/Kontext.Claude.Tests/Kontext.Claude.Tests.csproj" />
<Project Path="tests/Kontext.Claude.UnitTests/Kontext.Claude.UnitTests.csproj" />
<Project Path="tests/Kontext.Claude.IntegrationTests/Kontext.Claude.IntegrationTests.csproj" />
<Project Path="tests/Kontext.CompositionTests/Kontext.CompositionTests.csproj" />
<Project Path="tests/Kontext.ExternalTools.Tests/Kontext.ExternalTools.Tests.csproj" />
<Project Path="tests/Kontext.GitAutomation.Tests/Kontext.GitAutomation.Tests.csproj" />
<Project Path="tests/Kontext.GitHub.Tests/Kontext.GitHub.Tests.csproj" />
<Project Path="tests/Kontext.Presentation.Tests/Kontext.Presentation.Tests.csproj" />
<Project Path="tests/Kontext.Sessions.Tests/Kontext.Sessions.Tests.csproj" />
<Project Path="tests/Kontext.TerminalHost.Tests/Kontext.TerminalHost.Tests.csproj" />
<Project Path="tests/Kontext.ExternalTools.UnitTests/Kontext.ExternalTools.UnitTests.csproj" />
<Project Path="tests/Kontext.GitAutomation.UnitTests/Kontext.GitAutomation.UnitTests.csproj" />
<Project Path="tests/Kontext.GitAutomation.IntegrationTests/Kontext.GitAutomation.IntegrationTests.csproj" />
<Project Path="tests/Kontext.GitHub.UnitTests/Kontext.GitHub.UnitTests.csproj" />
<Project Path="tests/Kontext.Presentation.UnitTests/Kontext.Presentation.UnitTests.csproj" />
<Project Path="tests/Kontext.Sessions.UnitTests/Kontext.Sessions.UnitTests.csproj" />
<Project Path="tests/Kontext.Sessions.IntegrationTests/Kontext.Sessions.IntegrationTests.csproj" />
<Project Path="tests/Kontext.TerminalHost.UnitTests/Kontext.TerminalHost.UnitTests.csproj" />
<Project Path="tests/Kontext.TestSupport/Kontext.TestSupport.csproj" />
</Folder>
</Solution>
3 changes: 2 additions & 1 deletion src/Kontext.Claude/Kontext.Claude.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Kontext.Claude.Tests" />
<InternalsVisibleTo Include="Kontext.Claude.UnitTests" />
<InternalsVisibleTo Include="Kontext.Claude.IntegrationTests" />
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions src/Kontext.Contracts/IEnterpriseServerPrompt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Kontext.Domain;

namespace Kontext.Contracts;

public interface IEnterpriseServerPrompt
{
Task<EnterpriseCredentials?> AskAsync();
}
10 changes: 10 additions & 0 deletions src/Kontext.Contracts/IGitHubDeviceLogin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Kontext.Domain;

namespace Kontext.Contracts;

public interface IGitHubDeviceLogin
{
Task<DeviceCodeChallenge> StartAsync(GitHubServer? server = null, CancellationToken cancellationToken = default);

Task<string> WaitForAccessTokenAsync(DeviceCodeChallenge challenge, CancellationToken cancellationToken = default);
}
6 changes: 2 additions & 4 deletions src/Kontext.Contracts/IGitHubService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ namespace Kontext.Contracts;

public interface IGitHubService
{
GitHubUser? CurrentUser { get; }
Task<GitHubUser> AuthenticateAsync(string host, string accessToken, CancellationToken cancellationToken = default);

Task<GitHubUser> AuthenticateAsync(string accessToken, CancellationToken cancellationToken = default);

void SignOut();
void SignOut(string host);

Task<IReadOnlyList<GitHubIssue>> GetIssuesAsync(RepositoryRef? repository = null, CancellationToken cancellationToken = default);

Expand Down
8 changes: 5 additions & 3 deletions src/Kontext.Contracts/IGitHubTokenStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ namespace Kontext.Contracts;

public interface IGitHubTokenStore
{
string? Load();
string? Load(string host);

void Save(string token);
void Save(string host, string token);

void Clear();
void Clear(string host);

string? FindEnterpriseHost();
}
12 changes: 12 additions & 0 deletions src/Kontext.Domain/DeviceCodeChallenge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Kontext.Kernel;

namespace Kontext.Domain;

public sealed record DeviceCodeChallenge(
string DeviceCode,
string UserCode,
Uri VerificationUri,
DateTimeOffset ExpiresAt,
TimeSpan PollInterval,
string Host = GitHubHosts.Default,
string? ClientId = null);
3 changes: 3 additions & 0 deletions src/Kontext.Domain/EnterpriseCredentials.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Kontext.Domain;

public sealed record EnterpriseCredentials(GitHubServer Server, string Token);
Loading
Loading