Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
dab4837
doc
chrisbeamable Apr 29, 2026
280dea8
stage2
chrisbeamable Apr 29, 2026
bc4a867
working on tests before big human rewrite
chrisbeamable May 7, 2026
f3e8cd7
testing work
chrisbeamable May 15, 2026
9d412bb
mocks
chrisbeamable May 16, 2026
64fc8fa
more testing stuff
chrisbeamable May 17, 2026
68484e0
some fixes
chrisbeamable May 18, 2026
a977434
web runtime fun
chrisbeamable May 18, 2026
990beb1
web stuff play
chrisbeamable May 18, 2026
440d514
stable
chrisbeamable May 19, 2026
9255d86
checkpoint
chrisbeamable May 19, 2026
4400122
checkpoint
chrisbeamable May 20, 2026
4d3d052
checpoint
chrisbeamable May 20, 2026
c92abe7
checkpoint
chrisbeamable May 21, 2026
fced289
another checkpoint
chrisbeamable May 23, 2026
f249ced
lexer perf 1
chrisbeamable May 23, 2026
0870406
span lexer
chrisbeamable May 24, 2026
158c9cb
test benchmarks
chrisbeamable May 24, 2026
37f1fda
some perf
chrisbeamable May 24, 2026
dd97163
shared sb in lexer
chrisbeamable May 24, 2026
b44dfe5
cache error nodes
chrisbeamable May 24, 2026
b6b2b44
no generator
chrisbeamable May 24, 2026
07bdb13
token lex combo
chrisbeamable May 26, 2026
ae32b23
json benchmarks
chrisbeamable May 26, 2026
445754e
json perf
chrisbeamable May 26, 2026
2c640b5
json improvements
chrisbeamable May 28, 2026
a771eff
git
chrisbeamable May 28, 2026
067effe
folders
chrisbeamable May 29, 2026
d1fedcf
release
chrisbeamable May 29, 2026
5eeb5d8
blah
chrisbeamable May 31, 2026
0e2f37e
shaders
chrisbeamable Jun 2, 2026
323286b
eh
chrisbeamable Jun 11, 2026
be48b08
vm perf
chrisbeamable Jun 12, 2026
a706286
what am I even doing
chrisbeamable Jun 25, 2026
35ea6bd
changelog for 0.1.0
chrisbeamable Jun 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
94 changes: 94 additions & 0 deletions .github/workflows/ghostbot-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Builds the GhostBot desktop app (Tauri) for macOS + Windows and publishes the
# installers to a GitHub Release. The Playground's "Download GhostBot" button
# points at the latest release of this repo.
#
# Trigger manually (Actions → "GhostBot Release" → Run workflow, enter a version)
# or by pushing a tag like `ghostbot-v0.1.0`.
#
# Signing/notarization: the APPLE_* / WINDOWS_* secrets are optional. With them
# unset the build still succeeds but is UNSIGNED — macOS users must right-click →
# Open the first time, and Windows shows a SmartScreen prompt. Set the secrets to
# ship without those warnings.
name: GhostBot Release

on:
workflow_dispatch:
inputs:
version:
description: "Release version, e.g. 0.1.0"
required: true
push:
tags:
- "ghostbot-v*"

jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- platform: macos-latest
rust-targets: aarch64-apple-darwin,x86_64-apple-darwin
args: --target universal-apple-darwin
- platform: windows-latest
rust-targets: ""
args: ""
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20

- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.rust-targets }}

# llama-cpp-2 compiles llama.cpp via cmake — ensure it's present on both
# runners (preinstalled, but pinning avoids surprises).
- uses: lukka/get-cmake@latest

- uses: Swatinem/rust-cache@v2
with:
workspaces: ghostBot/src-tauri

- name: Install frontend dependencies
working-directory: ghostBot
run: npm ci

- name: Resolve version
id: ver
shell: bash
run: |
if [ -n "${{ github.event.inputs.version }}" ]; then
echo "v=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
else
echo "v=${GITHUB_REF_NAME#ghostbot-v}" >> "$GITHUB_OUTPUT"
fi

- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# macOS signing + notarization (optional; unset = unsigned build):
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
projectPath: ghostBot
tagName: ghostbot-v${{ steps.ver.outputs.v }}
releaseName: GhostBot v${{ steps.ver.outputs.v }}
releaseBody: |
GhostBot — the desktop AI runner for the Fade Playground.

Download the installer for your platform, install it, then enter its
join code in the Playground's **AI Models** tab to connect.

⚠️ Experimental. Builds may be unsigned — on macOS, right-click the app
and choose **Open** the first time.
releaseDraft: true
prerelease: true
args: ${{ matrix.args }}
12 changes: 11 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ project.lock.json
*.userosscache
*.sln.docstates

# install.sh WASM staging output
FadeBasic/FadeBasic.Export.Web/staging/

# Build results (scoped to .NET bin/obj so they don't match source folders named debug/release)
**/bin/[Dd]ebug/
**/bin/[Dd]ebugPublic/
Expand All @@ -27,6 +30,8 @@ project.lock.json
x64/
x86/
build/
!FadeBasic/FadeBasic.Export.Web/build/
!FadeBasic/FadeBasic.Export.Web/build/**
bld/
[Bb]in/
[Oo]bj/
Expand All @@ -40,4 +45,9 @@ msbuild.wrn

# Plugin signing material (JetBrains Marketplace)
*.pem
*.crt
*.crt

# BenchmarkDotNet output
**/BenchmarkDotNet.Artifacts/

node_modules/
3 changes: 2 additions & 1 deletion FadeBasic/ApplicationSupport/ApplicationSupport.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@

<ItemGroup>
<ProjectReference Include="..\FadeBasic\FadeBasic.csproj" />
<ProjectReference Include="..\LSP.Core\FadeBasic.LSP.Core.csproj" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="DocSite\index.html" LogicalName="docs/index.html"/>
<EmbeddedResource Include="DocSite\styles.css" LogicalName="docs/styles.css"/>
Expand Down
85 changes: 71 additions & 14 deletions FadeBasic/ApplicationSupport/Launch/LaunchableGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,59 @@ public class LaunchableGenerator
public const string TAG_MAIN = "__MAIN__";
public const string TAG_ENCODED_BYTECODE = "__ENCODED_BYTE_CODE__";
public const string TAG_ENCODED_DEBUGDATA = "__ENCODED_DEBUG_DATA__";
public const string TAG_ENCODED_TESTMANIFEST = "__ENCODED_TEST_MANIFEST__";
public const string TAG_COMMAND_ARRAY = "__COMMAND_ARR__";
public const string TEMPLATE_BYTECODE_TAB = " ";
public const string TEMPLATE_ENCODED_BYTE_VAR = "encodedByteCode";
public const string TEMPLATE_ENCODED_DEBUGDATA_VAR = "encodedDebugData";
public const string TEMPLATE_ENCODED_TESTMANIFEST_VAR = "encodedTestManifest";
public const string TEMPLATE_BYTECODE_VAR = "_byteCode";
public const string TEMPLATE_DEBUGDATA_VAR = "_debugData";
public const string TEMPLATE_TESTMANIFEST_VAR = "_testManifest";

// Default Main when FadeEnableTesting is off. Forwards args into the
// existing test-aware Launcher dispatcher (handles --fade-test=name etc.).
public static readonly string MainTemplate =
$@"
public static void Main(string[] args)
public static int Main(string[] args)
{{
Launcher.Run<{TAG_CLASSNAME}>();
return Launcher.Main<{TAG_CLASSNAME}>(args);
}}
";
public static readonly string ClassTemplate =

// Main when FadeEnableTesting is on. Routes Microsoft.Testing.Platform
// invocations (dotnet test, --list-tests, --filter, --server, ...) through
// FadeBasic.Testing.FadeTestApplicationBuilder; everything else still goes
// to the existing Launcher path so `dotnet run` and --fade-test keep working.
//
// Custom IFadeTestHost is picked up by attribute-based discovery: tag the
// class [FadeBasic.Testing.FadeTestHost] and FadeTestApplicationBuilder
// resolves it at startup. If none is found, DefaultFadeTestHost is used.
public static readonly string MainTemplateWithTesting =
$@"
public static int Main(string[] args)
{{
if (global::FadeBasic.Testing.FadeTestApplicationBuilder.IsTestInvocation(args))
{{
var instance = new {TAG_CLASSNAME}();
return global::FadeBasic.Testing.FadeTestApplicationBuilder
.RunAsync(instance, args)
.GetAwaiter().GetResult();
}}
return Launcher.Main<{TAG_CLASSNAME}>(args);
}}
";

public static readonly string ClassTemplate =
$@"// This is a generated file. Do not edit directly.

using {nameof(System)};
using {nameof(System)}.{nameof(System.Collections)}.{nameof(System.Collections.Generic)};
using {nameof(FadeBasic)};
using {nameof(FadeBasic)}.{nameof(FadeBasic.Launch)};
using {nameof(FadeBasic)}.{nameof(FadeBasic.Virtual)};

public class {TAG_CLASSNAME} : {nameof(ILaunchable)}
public partial class {TAG_CLASSNAME} : {nameof(ITestLaunchable)}
{{
{TAG_MAIN}

Expand All @@ -49,6 +79,8 @@ public class {TAG_CLASSNAME} : {nameof(ILaunchable)}

public DebugData DebugData => {TEMPLATE_DEBUGDATA_VAR};

public IReadOnlyList<TestManifestEntry> TestManifest => {TEMPLATE_TESTMANIFEST_VAR};

#region method table
private static readonly CommandCollection _collection = new CommandCollection(
{TAG_COMMAND_ARRAY}
Expand All @@ -64,37 +96,61 @@ public class {TAG_CLASSNAME} : {nameof(ILaunchable)}
protected byte[] {TEMPLATE_BYTECODE_VAR} = {nameof(LaunchUtil)}.{nameof(LaunchUtil.Unpack64)}({TEMPLATE_ENCODED_BYTE_VAR});
protected const string {TEMPLATE_ENCODED_BYTE_VAR} = {TAG_ENCODED_BYTECODE};
#endregion

#region testManifest
protected IReadOnlyList<TestManifestEntry> {TEMPLATE_TESTMANIFEST_VAR} = {nameof(LaunchUtil)}.{nameof(LaunchUtil.UnpackTestManifest)}({TEMPLATE_ENCODED_TESTMANIFEST_VAR});
protected const string {TEMPLATE_ENCODED_TESTMANIFEST_VAR} = {TAG_ENCODED_TESTMANIFEST};
#endregion
}}
";

public static void GenerateLaunchable(string className,
string filePath,
CodeUnit unit,
CommandCollection collection,
List<string> commandClasses,
public static void GenerateLaunchable(string className,
string filePath,
CodeUnit unit,
CommandCollection collection,
List<string> commandClasses,
bool includeMain=true,
bool generateDebug=false)
bool generateDebug=false,
bool enableTesting=false)
{
var compiler = unit.program.Compile(collection, new CompilerOptions
{
GenerateDebugData = generateDebug
});

// Stamp originating .fbasic file paths onto each test manifest entry
// before we pack it into the generated launchable. Multi-file projects
// need this so IDE Test Explorer (Stage 11H VSTest adapter) can
// source-link each test to the right file. CodeUnit always carries a
// SourceMap when it comes from the build-task / SDK pipelines.
FadeBasic.Launch.LaunchUtil.ApplySourceMap(compiler.TestManifest, unit.sourceMap);

var byteCode = compiler.Program.ToArray();
var src = ClassTemplate;

var byteCodeStr = LaunchUtil.Pack64(byteCode);
string byteCodeReplacement = "\"" + byteCodeStr + "\"";
var commandArray = GetCommandTable(commandClasses);


var debugDataStr = generateDebug ? LaunchUtil.PackDebugData(compiler.DebugData) : "";
string debugDataReplacement = "\"" + debugDataStr + "\"";

var main = includeMain ? MainTemplate : "";
src = src.Replace(TAG_MAIN, main);

// Always pack the test manifest. Empty when the source has no tests.
var testManifestStr = LaunchUtil.PackTestManifest(compiler.TestManifest);
string testManifestReplacement = "\"" + testManifestStr + "\"";

string mainBlock = "";
if (includeMain)
{
mainBlock = enableTesting ? MainTemplateWithTesting : MainTemplate;
}

src = src.Replace(TAG_MAIN, mainBlock);
src = src.Replace(TAG_COMMAND_ARRAY, commandArray);
src = src.Replace(TAG_ENCODED_BYTECODE, byteCodeReplacement);
src = src.Replace(TAG_ENCODED_DEBUGDATA, debugDataReplacement);
src = src.Replace(TAG_ENCODED_TESTMANIFEST, testManifestReplacement);
src = src.Replace(TAG_CLASSNAME, className);

var dir = Path.GetDirectoryName(filePath);
Expand All @@ -111,6 +167,7 @@ static string GetCommandTable(List<string> commandClasses)
}
return string.Join(", ", instantiates);
}

static string GetCommandTable(ProjectContext context)
{
// IMethod collection = new CommandCollection()
Expand Down
49 changes: 25 additions & 24 deletions FadeBasic/ApplicationSupport/Project/ProjectBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,39 +239,40 @@ public static (ProjectCommandInfo, AssemblyLoadContext) LoadCommands(string libP
var sources = new List<IMethodSource>();
var loadContext = new AssemblyLoadContext("metadata", isCollectible: true);

// TODO: technically there could be multiple lib paths, right? one for each library?
// var libPath = Path.GetDirectoryName(libraries[0].absoluteOutputDllPath);
// var libPath = AppContext.BaseDirectory;

// Probe the consumer's TargetDir (libPath) AND the directory of each
// loaded library DLL. A macro command in lib A can call into a sibling
// assembly B that A project-references; B sits next to A in A's own bin
// (or in the NuGet lib/ folder), but on a clean build it has not yet
// been copied into the consumer's TargetDir when this resolver runs.
var probeDirs = new List<string> { libPath };
foreach (var lib in libraries)
{
var dir = Path.GetDirectoryName(lib.absoluteOutputDllPath);
if (!string.IsNullOrEmpty(dir) && !probeDirs.Contains(dir))
probeDirs.Add(dir);
}

loadContext.Resolving += (assemblyContext, assemblyName) =>
{
if (assemblyName.FullName == typeof(IMethodSource).Assembly.GetName().FullName)
{
// log("!!! Trying to load common assembly.");
return typeof(IMethodSource).Assembly;
}


// log($"!!! Requested: [{assemblyName.FullName}]");
//log($"!!! Compared: [{typeof(IMethodSource).Assembly.GetName().FullName}]");

string candidatePath = Path.Combine(
libPath,
assemblyName.Name + ".dll");

// log($"!!! candidate-path=[{candidatePath}]");

if (!File.Exists(candidatePath))
return null;
foreach (var dir in probeDirs)
{
var candidatePath = Path.Combine(dir, assemblyName.Name + ".dll");
if (!File.Exists(candidatePath))
continue;

var foundName = AssemblyName.GetAssemblyName(candidatePath);
// log($"!!! candidate-name=[{foundName.Name}] vs requested=[{assemblyName.Name}]");

if (foundName.Name != assemblyName.Name)
return null;
var foundName = AssemblyName.GetAssemblyName(candidatePath);
if (foundName.Name != assemblyName.Name)
continue;

return assemblyContext.LoadIntoMemory(candidatePath);
}

// log($"!!! Proxied: [{candidatePath}]");
return assemblyContext.LoadIntoMemory(candidatePath);
return null;
};
using var _ = loadContext.EnterContextualReflection();

Expand Down
Loading
Loading