Add appstore/ to .gitignore

Private Mac App Store build, not for the public repo.
This commit is contained in:
iamtoruk 2026-05-18 05:42:54 -07:00
parent d8629c6978
commit b835b69cf4
19 changed files with 3 additions and 2867 deletions

3
.gitignore vendored
View file

@ -41,5 +41,8 @@ assets/discord-*.png
# Desktop app experiments
desktop/
# Mac App Store app (private)
appstore/
# WIP / not ready
src/summit.ts

View file

@ -1,492 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
2ECF1C27FA4DDA36495BA68C /* litellm-snapshot.json in Resources */ = {isa = PBXBuildFile; fileRef = 8AAE65B7A750492C322FA5E7 /* litellm-snapshot.json */; };
3007410303C4D0434669B50E /* ProviderData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E37160EB1A5432B8293AA2D /* ProviderData.swift */; };
3DB70769F5E327B7D81A4A2F /* ParserFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 166E0C0CE8503D3A2D2879E0 /* ParserFactory.swift */; };
3F6F4EDBAB788441F22F21D4 /* PricingEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4B76B18A1F16C3BFEA8852 /* PricingEngine.swift */; };
493862507A93C42271509F33 /* MenuBarPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505DEE70935AC5B6EFD96342 /* MenuBarPopover.swift */; };
4D9E1DC41999837FD6234105 /* SessionDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE26E7C025F9E9B76F1EF5E2 /* SessionDiscovery.swift */; };
81BDD9D51CF36753E4DE4039 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A081AEB9C62823EA049C381 /* Theme.swift */; };
8FF7627DBD6A1925F25B6998 /* DashboardSections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D18497715889FD2E28988D6 /* DashboardSections.swift */; };
A9920B49759503DA503CDAE6 /* SessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B016A43AF196CE6A493C7538 /* SessionStore.swift */; };
CA53CB4F02B48520B5632AAF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8F460B9E6390F3FB1E15FD87 /* Assets.xcassets */; };
DFEEF70015C2BDAECF8380BB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC1764E5E823EB826E9A95BA /* SettingsView.swift */; };
FFDB0DFF1ED97F07AB94BA08 /* CodeBurnProApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1E8B2BAC761E675B36E779 /* CodeBurnProApp.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
BE21D11E0B437BB5CC92D560 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 98C12FE29026B86BB7691704 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 4644213E89A759CA3536CE98;
remoteInfo = CodeBurnPro;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
0E37160EB1A5432B8293AA2D /* ProviderData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderData.swift; sourceTree = "<group>"; };
166E0C0CE8503D3A2D2879E0 /* ParserFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserFactory.swift; sourceTree = "<group>"; };
1A081AEB9C62823EA049C381 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
1D18497715889FD2E28988D6 /* DashboardSections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardSections.swift; sourceTree = "<group>"; };
3C4B76B18A1F16C3BFEA8852 /* PricingEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PricingEngine.swift; sourceTree = "<group>"; };
40A6F8098AC554850CC48A55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
505DEE70935AC5B6EFD96342 /* MenuBarPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarPopover.swift; sourceTree = "<group>"; };
5A1E8B2BAC761E675B36E779 /* CodeBurnProApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeBurnProApp.swift; sourceTree = "<group>"; };
8AAE65B7A750492C322FA5E7 /* litellm-snapshot.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "litellm-snapshot.json"; sourceTree = "<group>"; };
8F460B9E6390F3FB1E15FD87 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
B016A43AF196CE6A493C7538 /* SessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionStore.swift; sourceTree = "<group>"; };
B81E89E9FB0BDF09FBAD945C /* CodeBurnPro.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CodeBurnPro.app; sourceTree = BUILT_PRODUCTS_DIR; };
D8D13C20D2180A5466744F05 /* CodeBurnProTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CodeBurnProTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
DC63231DAA219A843397A91C /* CodeBurnPro.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CodeBurnPro.entitlements; sourceTree = "<group>"; };
DE26E7C025F9E9B76F1EF5E2 /* SessionDiscovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDiscovery.swift; sourceTree = "<group>"; };
FC1764E5E823EB826E9A95BA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
010E84A815B086F865EE139D /* Tests */ = {
isa = PBXGroup;
children = (
);
path = Tests;
sourceTree = "<group>";
};
2743809DB6BC37FB231BF136 /* Views */ = {
isa = PBXGroup;
children = (
1D18497715889FD2E28988D6 /* DashboardSections.swift */,
505DEE70935AC5B6EFD96342 /* MenuBarPopover.swift */,
FC1764E5E823EB826E9A95BA /* SettingsView.swift */,
1A081AEB9C62823EA049C381 /* Theme.swift */,
);
path = Views;
sourceTree = "<group>";
};
354902BC5C1D6FA2706BB87B /* App */ = {
isa = PBXGroup;
children = (
DC63231DAA219A843397A91C /* CodeBurnPro.entitlements */,
5A1E8B2BAC761E675B36E779 /* CodeBurnProApp.swift */,
40A6F8098AC554850CC48A55 /* Info.plist */,
);
path = App;
sourceTree = "<group>";
};
370A693B64F8F4F443EA6F75 /* Sources */ = {
isa = PBXGroup;
children = (
354902BC5C1D6FA2706BB87B /* App */,
49E1E2079DE6A12CA955D709 /* Core */,
A8136629A97D7302328F9226 /* Models */,
B53474647E57314BBB4A5BF9 /* Resources */,
2743809DB6BC37FB231BF136 /* Views */,
);
path = Sources;
sourceTree = "<group>";
};
49E1E2079DE6A12CA955D709 /* Core */ = {
isa = PBXGroup;
children = (
166E0C0CE8503D3A2D2879E0 /* ParserFactory.swift */,
3C4B76B18A1F16C3BFEA8852 /* PricingEngine.swift */,
DE26E7C025F9E9B76F1EF5E2 /* SessionDiscovery.swift */,
);
path = Core;
sourceTree = "<group>";
};
A8136629A97D7302328F9226 /* Models */ = {
isa = PBXGroup;
children = (
0E37160EB1A5432B8293AA2D /* ProviderData.swift */,
B016A43AF196CE6A493C7538 /* SessionStore.swift */,
);
path = Models;
sourceTree = "<group>";
};
AE4CF1BC0160C3FD593C6DD6 = {
isa = PBXGroup;
children = (
370A693B64F8F4F443EA6F75 /* Sources */,
010E84A815B086F865EE139D /* Tests */,
D5B44508B31DEDE68BF93CE7 /* Products */,
);
sourceTree = "<group>";
};
B53474647E57314BBB4A5BF9 /* Resources */ = {
isa = PBXGroup;
children = (
8F460B9E6390F3FB1E15FD87 /* Assets.xcassets */,
8AAE65B7A750492C322FA5E7 /* litellm-snapshot.json */,
);
path = Resources;
sourceTree = "<group>";
};
D5B44508B31DEDE68BF93CE7 /* Products */ = {
isa = PBXGroup;
children = (
B81E89E9FB0BDF09FBAD945C /* CodeBurnPro.app */,
D8D13C20D2180A5466744F05 /* CodeBurnProTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
4644213E89A759CA3536CE98 /* CodeBurnPro */ = {
isa = PBXNativeTarget;
buildConfigurationList = 39BA4438AB143ED6916F0111 /* Build configuration list for PBXNativeTarget "CodeBurnPro" */;
buildPhases = (
C9DC56CFCB5B5C9F97B3143E /* Sources */,
7325A33B9C9197DA66E1F3BE /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = CodeBurnPro;
packageProductDependencies = (
);
productName = CodeBurnPro;
productReference = B81E89E9FB0BDF09FBAD945C /* CodeBurnPro.app */;
productType = "com.apple.product-type.application";
};
C1F291F2AC223737AE163858 /* CodeBurnProTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 27D48476E54F490BB2D51D5D /* Build configuration list for PBXNativeTarget "CodeBurnProTests" */;
buildPhases = (
4AAAA6AF0579CE4D2C32BF22 /* Sources */,
);
buildRules = (
);
dependencies = (
90669F9181C61FA9ED854A6E /* PBXTargetDependency */,
);
name = CodeBurnProTests;
packageProductDependencies = (
);
productName = CodeBurnProTests;
productReference = D8D13C20D2180A5466744F05 /* CodeBurnProTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
98C12FE29026B86BB7691704 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1600;
TargetAttributes = {
4644213E89A759CA3536CE98 = {
DevelopmentTeam = "";
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 82F59EE0DDE26D399B09396D /* Build configuration list for PBXProject "CodeBurnPro" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Base,
en,
);
mainGroup = AE4CF1BC0160C3FD593C6DD6;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = D5B44508B31DEDE68BF93CE7 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
4644213E89A759CA3536CE98 /* CodeBurnPro */,
C1F291F2AC223737AE163858 /* CodeBurnProTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
7325A33B9C9197DA66E1F3BE /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CA53CB4F02B48520B5632AAF /* Assets.xcassets in Resources */,
2ECF1C27FA4DDA36495BA68C /* litellm-snapshot.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
4AAAA6AF0579CE4D2C32BF22 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
C9DC56CFCB5B5C9F97B3143E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FFDB0DFF1ED97F07AB94BA08 /* CodeBurnProApp.swift in Sources */,
8FF7627DBD6A1925F25B6998 /* DashboardSections.swift in Sources */,
493862507A93C42271509F33 /* MenuBarPopover.swift in Sources */,
3DB70769F5E327B7D81A4A2F /* ParserFactory.swift in Sources */,
3F6F4EDBAB788441F22F21D4 /* PricingEngine.swift in Sources */,
3007410303C4D0434669B50E /* ProviderData.swift in Sources */,
4D9E1DC41999837FD6234105 /* SessionDiscovery.swift in Sources */,
A9920B49759503DA503CDAE6 /* SessionStore.swift in Sources */,
DFEEF70015C2BDAECF8380BB /* SettingsView.swift in Sources */,
81BDD9D51CF36753E4DE4039 /* Theme.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
90669F9181C61FA9ED854A6E /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 4644213E89A759CA3536CE98 /* CodeBurnPro */;
targetProxy = BE21D11E0B437BB5CC92D560 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
16636C9409E6025D87CC6B93 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Sources/App/CodeBurnPro.entitlements;
CODE_SIGN_IDENTITY = "Apple Development: toruk_makto1406@icloud.com (T3289GPL2P)";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = Sources/App/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.agentseal.codeburn-pro";
PRODUCT_NAME = "CodeBurn Pro";
SDKROOT = macosx;
};
name = Debug;
};
28D3BCE119F61F0FC7E45532 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
COMBINE_HIDPI_IMAGES = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.agentseal.codeburn-pro-tests";
SDKROOT = macosx;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CodeBurnPro.app/Contents/MacOS/CodeBurnPro";
};
name = Debug;
};
669A9FA55E2ED179ED72D24A /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 6.0;
};
name = Release;
};
6B26F1606268EC787A0BEF18 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Sources/App/CodeBurnPro.entitlements;
CODE_SIGN_IDENTITY = "Apple Development: toruk_makto1406@icloud.com (T3289GPL2P)";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = Sources/App/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.agentseal.codeburn-pro";
PRODUCT_NAME = "CodeBurn Pro";
SDKROOT = macosx;
};
name = Release;
};
6C4287EB5F1EF379F9E640B0 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"DEBUG=1",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 6.0;
};
name = Debug;
};
A63E55710D34F3B756F6E463 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
COMBINE_HIDPI_IMAGES = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.agentseal.codeburn-pro-tests";
SDKROOT = macosx;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CodeBurnPro.app/Contents/MacOS/CodeBurnPro";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
27D48476E54F490BB2D51D5D /* Build configuration list for PBXNativeTarget "CodeBurnProTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
28D3BCE119F61F0FC7E45532 /* Debug */,
A63E55710D34F3B756F6E463 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
39BA4438AB143ED6916F0111 /* Build configuration list for PBXNativeTarget "CodeBurnPro" */ = {
isa = XCConfigurationList;
buildConfigurations = (
16636C9409E6025D87CC6B93 /* Debug */,
6B26F1606268EC787A0BEF18 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
82F59EE0DDE26D399B09396D /* Build configuration list for PBXProject "CodeBurnPro" */ = {
isa = XCConfigurationList;
buildConfigurations = (
6C4287EB5F1EF379F9E640B0 /* Debug */,
669A9FA55E2ED179ED72D24A /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */
};
rootObject = 98C12FE29026B86BB7691704 /* Project object */;
}

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View file

@ -1,35 +0,0 @@
import SwiftUI
@main
struct CodeBurnProApp: App {
@StateObject private var store = SessionStore()
var body: some Scene {
MenuBarExtra {
MenuBarPopover(store: store)
} label: {
HStack(spacing: 3) {
Image(systemName: "flame.fill")
if !store.needsOnboarding {
Text(store.todayCost.asMenuBarCurrency())
.monospacedDigit()
}
}
}
.menuBarExtraStyle(.window)
Settings {
SettingsView(store: store)
}
}
}
extension Double {
func asMenuBarCurrency() -> String {
if self <= 0 { return "$0" }
if self >= 1000 { return String(format: "$%.0fK", self / 1000) }
if self >= 100 { return String(format: "$%.0f", self) }
if self >= 10 { return String(format: "$%.1f", self) }
return String(format: "$%.2f", self)
}
}

View file

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>CodeBurn Pro</string>
<key>CFBundleDisplayName</key>
<string>CodeBurn Pro</string>
<key>CFBundleIdentifier</key>
<string>com.agentseal.codeburn-pro</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>LSUIElement</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright 2026 AgentSeal. All rights reserved.</string>
</dict>
</plist>

View file

@ -1,307 +0,0 @@
import Foundation
enum ParserFactory {
static func parser(for provider: String) -> SessionParser {
switch provider {
case "claude": ClaudeParser()
case "codex": CodexParser()
case "copilot": CopilotParser()
default: GenericJSONLParser(provider: provider)
}
}
}
protocol SessionParser {
func parseSessions(at paths: [URL], in dateRange: ClosedRange<Date>) -> [ParsedSession]
}
// MARK: - Claude JSONL parser
struct ClaudeParser: SessionParser {
func parseSessions(at paths: [URL], in dateRange: ClosedRange<Date>) -> [ParsedSession] {
paths.compactMap { parseClaudeSession(at: $0, in: dateRange) }
}
private func parseClaudeSession(at url: URL, in dateRange: ClosedRange<Date>) -> ParsedSession? {
guard let handle = try? FileHandle(forReadingFrom: url) else { return nil }
defer { try? handle.close() }
guard let size = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize, size < 100_000_000 else { return nil }
guard let data = try? Data(contentsOf: url),
let text = String(data: data, encoding: .utf8)
else { return nil }
let lines = text.components(separatedBy: .newlines).filter { !$0.isEmpty }
var turns: [ParsedTurn] = []
var firstTimestamp: Date?
let isoFormatter = ISO8601DateFormatter()
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
for line in lines {
guard let json = try? JSONSerialization.jsonObject(with: Data(line.utf8)) as? [String: Any] else { continue }
guard let message = json["message"] as? [String: Any],
let role = message["role"] as? String, role == "assistant"
else { continue }
let timestamp = (json["timestamp"] as? String).flatMap { isoFormatter.date(from: $0) }
?? (message["timestamp"] as? String).flatMap { isoFormatter.date(from: $0) }
?? Date()
if firstTimestamp == nil { firstTimestamp = timestamp }
guard dateRange.contains(timestamp) else { continue }
let usage = message["usage"] as? [String: Any] ?? [:]
let inputTokens = usage["input_tokens"] as? Int ?? 0
let outputTokens = usage["output_tokens"] as? Int ?? 0
let cacheRead = usage["cache_read_input_tokens"] as? Int ?? 0
let cacheWrite = usage["cache_creation_input_tokens"] as? Int ?? 0
let speed = usage["speed"] as? String ?? "standard"
let cacheCreation = usage["cache_creation"] as? [String: Any]
let oneHourCacheWrite = cacheCreation?["ephemeral_1h_input_tokens"] as? Int ?? 0
let model = message["model"] as? String ?? "unknown"
let cost = PricingEngine.cost(
model: model, input: inputTokens, output: outputTokens,
cacheRead: cacheRead, cacheWrite: cacheWrite,
oneHourCacheWrite: oneHourCacheWrite, speed: speed
)
let toolUse = (message["content"] as? [[String: Any]])?.compactMap { $0["name"] as? String } ?? []
turns.append(ParsedTurn(
timestamp: timestamp, model: model,
inputTokens: inputTokens, outputTokens: outputTokens,
cacheReadTokens: cacheRead, cacheWriteTokens: cacheWrite,
cost: cost, toolCalls: toolUse
))
}
guard !turns.isEmpty else { return nil }
let project = url.deletingLastPathComponent().lastPathComponent
return ParsedSession(
id: url.lastPathComponent,
project: project,
provider: "claude",
startDate: firstTimestamp ?? Date(),
calls: turns.count,
cost: turns.reduce(0) { $0 + $1.cost },
inputTokens: turns.reduce(0) { $0 + $1.inputTokens },
outputTokens: turns.reduce(0) { $0 + $1.outputTokens },
cacheReadTokens: turns.reduce(0) { $0 + $1.cacheReadTokens },
cacheWriteTokens: turns.reduce(0) { $0 + $1.cacheWriteTokens },
model: turns.first?.model ?? "unknown",
turns: turns
)
}
}
// MARK: - Codex JSONL parser
struct CodexParser: SessionParser {
func parseSessions(at paths: [URL], in dateRange: ClosedRange<Date>) -> [ParsedSession] {
paths.compactMap { parseCodexSession(at: $0, in: dateRange) }
}
private func parseCodexSession(at url: URL, in dateRange: ClosedRange<Date>) -> ParsedSession? {
guard let data = try? Data(contentsOf: url),
let text = String(data: data, encoding: .utf8)
else { return nil }
let lines = text.components(separatedBy: .newlines).filter { !$0.isEmpty }
var turns: [ParsedTurn] = []
var firstTimestamp: Date?
var sessionModel = "unknown"
var currentModel = "unknown"
var prevInput = 0, prevCached = 0, prevOutput = 0
let isoFormatter = ISO8601DateFormatter()
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
for line in lines {
guard let json = try? JSONSerialization.jsonObject(with: Data(line.utf8)) as? [String: Any],
let type = json["type"] as? String
else { continue }
let payload = json["payload"] as? [String: Any] ?? [:]
if type == "session_meta" {
if let m = payload["model"] as? String, !m.isEmpty { sessionModel = m; currentModel = m }
let originator = (payload["originator"] as? String ?? "").lowercased()
guard originator.hasPrefix("codex") else { return nil }
continue
}
if type == "turn_context" {
if let m = payload["model"] as? String, !m.isEmpty { currentModel = m }
continue
}
guard type == "event_msg",
let payloadType = payload["type"] as? String, payloadType == "token_count"
else { continue }
let ts = (json["timestamp"] as? String).flatMap { isoFormatter.date(from: $0) } ?? Date()
if firstTimestamp == nil { firstTimestamp = ts }
guard dateRange.contains(ts) else { continue }
let info = payload["info"] as? [String: Any] ?? [:]
let inputTokens: Int
let cachedTokens: Int
let outputTokens: Int
if let last = info["last_token_usage"] as? [String: Any] {
inputTokens = last["input_tokens"] as? Int ?? 0
cachedTokens = last["cached_input_tokens"] as? Int ?? 0
outputTokens = last["output_tokens"] as? Int ?? 0
} else if let total = info["total_token_usage"] as? [String: Any] {
let totalIn = total["input_tokens"] as? Int ?? 0
let totalCached = total["cached_input_tokens"] as? Int ?? 0
let totalOut = total["output_tokens"] as? Int ?? 0
inputTokens = max(0, totalIn - prevInput)
cachedTokens = max(0, totalCached - prevCached)
outputTokens = max(0, totalOut - prevOutput)
prevInput = totalIn
prevCached = totalCached
prevOutput = totalOut
} else {
continue
}
let uncachedInput = max(0, inputTokens - cachedTokens)
let cost = PricingEngine.cost(
model: currentModel, input: uncachedInput, output: outputTokens,
cacheRead: cachedTokens
)
turns.append(ParsedTurn(
timestamp: ts, model: currentModel,
inputTokens: uncachedInput, outputTokens: outputTokens,
cacheReadTokens: cachedTokens, cacheWriteTokens: 0,
cost: cost, toolCalls: []
))
}
guard !turns.isEmpty else { return nil }
let project = url.deletingLastPathComponent().lastPathComponent
return ParsedSession(
id: url.lastPathComponent, project: project, provider: "codex",
startDate: firstTimestamp ?? Date(), calls: turns.count,
cost: turns.reduce(0) { $0 + $1.cost },
inputTokens: turns.reduce(0) { $0 + $1.inputTokens },
outputTokens: turns.reduce(0) { $0 + $1.outputTokens },
cacheReadTokens: turns.reduce(0) { $0 + $1.cacheReadTokens },
cacheWriteTokens: 0,
model: sessionModel, turns: turns
)
}
}
// MARK: - Copilot parser
struct CopilotParser: SessionParser {
private static let charsPerToken = 4
func parseSessions(at paths: [URL], in dateRange: ClosedRange<Date>) -> [ParsedSession] {
paths.compactMap { parseCopilotSession(at: $0, in: dateRange) }
}
private func parseCopilotSession(at url: URL, in dateRange: ClosedRange<Date>) -> ParsedSession? {
guard let data = try? Data(contentsOf: url),
let text = String(data: data, encoding: .utf8)
else { return nil }
let lines = text.components(separatedBy: .newlines).filter { !$0.isEmpty }
var turns: [ParsedTurn] = []
var firstTimestamp: Date?
for line in lines {
guard let json = try? JSONSerialization.jsonObject(with: Data(line.utf8)) as? [String: Any] else { continue }
guard let type = json["type"] as? String else { continue }
let data = json["data"] as? [String: Any] ?? [:]
let ts = (json["timestamp"] as? TimeInterval).map { Date(timeIntervalSince1970: $0 / 1000) }
?? (json["timestamp"] as? String).flatMap { ISO8601DateFormatter().date(from: $0) }
?? Date()
if firstTimestamp == nil { firstTimestamp = ts }
guard dateRange.contains(ts) else { continue }
if type == "response" || type == "assistant" {
let content = data["content"] as? String ?? ""
let outputTokens = data["outputTokens"] as? Int ?? (content.count + Self.charsPerToken - 1) / Self.charsPerToken
let model = inferModel(from: data)
let cost = PricingEngine.cost(model: model, input: 0, output: outputTokens)
turns.append(ParsedTurn(
timestamp: ts, model: model,
inputTokens: 0, outputTokens: outputTokens,
cacheReadTokens: 0, cacheWriteTokens: 0,
cost: cost, toolCalls: []
))
}
}
guard !turns.isEmpty else { return nil }
let project = url.deletingLastPathComponent().lastPathComponent
return ParsedSession(
id: url.lastPathComponent, project: project, provider: "copilot",
startDate: firstTimestamp ?? Date(), calls: turns.count,
cost: turns.reduce(0) { $0 + $1.cost },
inputTokens: 0,
outputTokens: turns.reduce(0) { $0 + $1.outputTokens },
cacheReadTokens: 0, cacheWriteTokens: 0,
model: turns.first?.model ?? "copilot-auto", turns: turns
)
}
private func inferModel(from data: [String: Any]) -> String {
if let model = data["model"] as? String, !model.isEmpty { return model }
let toolRequests = data["toolRequests"] as? [[String: Any]] ?? []
for req in toolRequests {
if let id = req["id"] as? String {
if id.hasPrefix("toolu_") { return "copilot-anthropic-auto" }
if id.hasPrefix("call_") { return "copilot-openai-auto" }
}
}
return "copilot-auto"
}
}
// MARK: - Generic fallback
struct GenericJSONLParser: SessionParser {
let provider: String
func parseSessions(at paths: [URL], in dateRange: ClosedRange<Date>) -> [ParsedSession] {
paths.compactMap { path in
guard let data = try? Data(contentsOf: path),
let text = String(data: data, encoding: .utf8)
else { return nil }
let lines = text.components(separatedBy: .newlines).filter { !$0.isEmpty }
var cost = 0.0
var calls = 0
for line in lines {
guard let json = try? JSONSerialization.jsonObject(with: Data(line.utf8)) as? [String: Any] else { continue }
let usage = json["usage"] as? [String: Any] ?? [:]
let input = usage["input_tokens"] as? Int ?? 0
let output = usage["output_tokens"] as? Int ?? 0
let model = json["model"] as? String ?? "unknown"
cost += PricingEngine.cost(model: model, input: input, output: output)
calls += 1
}
guard calls > 0 else { return nil }
return ParsedSession(
id: path.lastPathComponent, project: path.deletingLastPathComponent().lastPathComponent,
provider: provider, startDate: Date(), calls: calls, cost: cost,
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
model: "unknown", turns: []
)
}
}
}

View file

@ -1,121 +0,0 @@
import Foundation
enum PricingEngine {
struct ModelCosts {
let inputPerToken: Double
let outputPerToken: Double
let cacheWritePerToken: Double
let cacheReadPerToken: Double
let fastMultiplier: Double
}
private static let oneHourCacheWriteMultiplier = 1.6
private static let fastMultipliers: [String: Double] = [
"claude-opus-4-7": 6,
"claude-opus-4-6": 6,
]
private static let builtinAliases: [String: String] = [
"claude-4.6-opus": "claude-opus-4-6",
"claude-4.6-opus-fast-mode": "claude-opus-4-6",
"claude-4.6-opus-high": "claude-opus-4-6",
"claude-4.6-opus-low": "claude-opus-4-6",
"claude-4.6-opus-medium": "claude-opus-4-6",
"claude-4.6-opus-high-thinking": "claude-opus-4-6",
"claude-4.7-opus": "claude-opus-4-7",
"claude-opus-4-7-thinking-high": "claude-opus-4-7",
"claude-4.5-opus": "claude-opus-4-5",
"claude-4.5-opus-high": "claude-opus-4-5",
"claude-4.5-opus-low": "claude-opus-4-5",
"claude-4.5-opus-medium": "claude-opus-4-5",
"claude-4.5-opus-high-thinking": "claude-opus-4-5",
"claude-4-opus": "claude-opus-4",
"anthropic--claude-4.6-opus": "claude-opus-4-6",
"anthropic--claude-4.5-opus": "claude-opus-4-5",
"claude-opus-4.7": "claude-opus-4-7",
"claude-opus-4.6": "claude-opus-4-6",
"claude-opus-4.5": "claude-opus-4-5",
]
private static let snapshot: [String: ModelCosts] = {
guard let url = Bundle.main.url(forResource: "litellm-snapshot", withExtension: "json"),
let data = try? Data(contentsOf: url),
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else { return [:] }
var map: [String: ModelCosts] = [:]
for (name, raw) in dict {
guard let arr = raw as? [Any], arr.count >= 2,
let input = arr[0] as? Double, input >= 0,
let output = arr[1] as? Double, output >= 0
else { continue }
let cacheWrite = (arr.count > 2 ? arr[2] as? Double : nil) ?? (input * 1.25)
let cacheRead = (arr.count > 3 ? arr[3] as? Double : nil) ?? (input * 0.1)
map[name] = ModelCosts(
inputPerToken: input,
outputPerToken: output,
cacheWritePerToken: cacheWrite,
cacheReadPerToken: cacheRead,
fastMultiplier: fastMultipliers[name] ?? 1
)
}
return map
}()
private static let sortedKeys: [String] = {
Array(snapshot.keys).sorted { $0.count > $1.count }
}()
static func cost(
model: String,
input: Int,
output: Int,
cacheRead: Int = 0,
cacheWrite: Int = 0,
oneHourCacheWrite: Int = 0,
speed: String = "standard"
) -> Double {
guard let costs = lookup(model) else { return 0 }
let multiplier = speed == "fast" ? costs.fastMultiplier : 1.0
let safeOneHour = max(0, oneHourCacheWrite)
let totalCW = max(max(0, cacheWrite), safeOneHour)
let fiveMinCW = max(0, totalCW - safeOneHour)
return multiplier * (
Double(max(0, input)) * costs.inputPerToken
+ Double(max(0, output)) * costs.outputPerToken
+ Double(fiveMinCW) * costs.cacheWritePerToken
+ Double(safeOneHour) * costs.cacheWritePerToken * oneHourCacheWriteMultiplier
+ Double(max(0, cacheRead)) * costs.cacheReadPerToken
)
}
private static func lookup(_ model: String) -> ModelCosts? {
let stripped = model
.replacingOccurrences(of: #"@.*$"#, with: "", options: .regularExpression)
.replacingOccurrences(of: #"-\d{8}$"#, with: "", options: .regularExpression)
if let costs = snapshot[stripped] { return costs }
let canonical = resolveCanonical(stripped)
if let costs = snapshot[canonical] { return costs }
for key in sortedKeys {
if canonical == key || canonical.hasPrefix(key + "-") {
return snapshot[key]
}
}
return nil
}
private static func resolveCanonical(_ model: String) -> String {
var name = model
if let slashRange = name.range(of: "/") {
name = String(name[slashRange.upperBound...])
}
if let resolved = builtinAliases[name] { return resolved }
return name
}
}

View file

@ -1,118 +0,0 @@
import Foundation
enum SessionDiscovery {
static func discoverAll(under root: URL, modifiedAfter cutoff: Date) -> [(String, [URL])] {
var results: [(String, [URL])] = []
for spec in providerSpecs {
let paths = spec.discover(root, cutoff)
if !paths.isEmpty {
results.append((spec.name, paths))
}
}
return results
}
private static let providerSpecs: [ProviderSpec] = [
.claude, .codex, .copilotLegacy, .copilotVSCode,
.cursor, .gemini, .cline, .rooCode, .kiloCode,
]
}
struct ProviderSpec: Sendable {
let name: String
let discover: @Sendable (URL, Date) -> [URL]
}
extension ProviderSpec {
static let claude = ProviderSpec(name: "claude") { root, cutoff in
let dir = root.appendingPathComponent(".claude/projects")
return findJSONLFiles(under: dir, maxDepth: 4, modifiedAfter: cutoff)
}
static let codex = ProviderSpec(name: "codex") { root, cutoff in
let dir = root.appendingPathComponent(".codex")
return findJSONLFiles(under: dir, maxDepth: 6, modifiedAfter: cutoff)
}
static let copilotLegacy = ProviderSpec(name: "copilot") { root, cutoff in
let dir = root.appendingPathComponent(".copilot/session-state")
return findJSONLFiles(under: dir, maxDepth: 3, modifiedAfter: cutoff)
}
static let copilotVSCode = ProviderSpec(name: "copilot") { root, cutoff in
let base = root.appendingPathComponent("Library/Application Support/Code/User/workspaceStorage")
guard FileManager.default.fileExists(atPath: base.path) else { return [] }
var results: [URL] = []
let fm = FileManager.default
guard let workspaces = try? fm.contentsOfDirectory(at: base, includingPropertiesForKeys: nil) else { return results }
for ws in workspaces {
let transcripts = ws.appendingPathComponent("GitHub.copilot-chat/transcripts")
results.append(contentsOf: findJSONLFiles(under: transcripts, maxDepth: 1, modifiedAfter: cutoff))
}
return results
}
static let cursor = ProviderSpec(name: "cursor") { root, _ in
let paths = [
root.appendingPathComponent("Library/Application Support/Cursor/User/globalStorage/cursor.db"),
root.appendingPathComponent(".cursor/globalStorage/cursor.db"),
]
return paths.filter { FileManager.default.fileExists(atPath: $0.path) }
}
static let gemini = ProviderSpec(name: "gemini") { root, cutoff in
let dir = root.appendingPathComponent(".gemini/sessions")
return findJSONFiles(under: dir, maxDepth: 2, modifiedAfter: cutoff)
}
static let cline = ProviderSpec(name: "cline") { root, cutoff in
let base = root.appendingPathComponent("Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/tasks")
return findJSONFiles(under: base, maxDepth: 2, modifiedAfter: cutoff, named: "ui_messages.json")
}
static let rooCode = ProviderSpec(name: "roo-code") { root, cutoff in
let base = root.appendingPathComponent("Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/tasks")
return findJSONFiles(under: base, maxDepth: 2, modifiedAfter: cutoff, named: "ui_messages.json")
}
static let kiloCode = ProviderSpec(name: "kilo-code") { root, cutoff in
let base = root.appendingPathComponent("Library/Application Support/Code/User/globalStorage/kilocode.kilo-code/tasks")
return findJSONFiles(under: base, maxDepth: 2, modifiedAfter: cutoff, named: "ui_messages.json")
}
}
private func findJSONLFiles(under dir: URL, maxDepth: Int, modifiedAfter cutoff: Date) -> [URL] {
findFiles(under: dir, maxDepth: maxDepth, extensions: ["jsonl"], modifiedAfter: cutoff)
}
private func findJSONFiles(under dir: URL, maxDepth: Int, modifiedAfter cutoff: Date, named: String? = nil) -> [URL] {
findFiles(under: dir, maxDepth: maxDepth, extensions: ["json"], modifiedAfter: cutoff, named: named)
}
private func findFiles(under dir: URL, maxDepth: Int, extensions: [String], modifiedAfter cutoff: Date, named: String? = nil) -> [URL] {
let fm = FileManager.default
guard fm.fileExists(atPath: dir.path) else { return [] }
var results: [URL] = []
guard let enumerator = fm.enumerator(
at: dir,
includingPropertiesForKeys: [.isRegularFileKey, .contentModificationDateKey],
options: [.skipsHiddenFiles]
) else { return [] }
for case let url as URL in enumerator {
if enumerator.level > maxDepth { enumerator.skipDescendants(); continue }
if let named, url.lastPathComponent != named { continue }
guard extensions.contains(url.pathExtension) else { continue }
if let modDate = try? url.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate,
modDate < cutoff { continue }
results.append(url)
}
return results.sorted { a, b in
let aDate = (try? a.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast
let bDate = (try? b.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast
return aDate > bDate
}
}

View file

@ -1,70 +0,0 @@
import Foundation
struct ProviderData: Identifiable, Sendable {
let id = UUID()
let name: String
var sessions: [ParsedSession]
var totalCost: Double { sessions.reduce(0) { $0 + $1.cost } }
var totalCalls: Int { sessions.reduce(0) { $0 + $1.calls } }
init(name: String, sessions: [ParsedSession] = []) {
self.name = name
self.sessions = sessions
}
}
struct ParsedSession: Identifiable, Sendable {
let id: String
let project: String
let provider: String
let startDate: Date
let calls: Int
let cost: Double
let inputTokens: Int
let outputTokens: Int
let cacheReadTokens: Int
let cacheWriteTokens: Int
let model: String
let turns: [ParsedTurn]
}
struct ParsedTurn: Sendable {
let timestamp: Date
let model: String
let inputTokens: Int
let outputTokens: Int
let cacheReadTokens: Int
let cacheWriteTokens: Int
let cost: Double
let toolCalls: [String]
}
extension SessionStore.Period {
var dateRange: ClosedRange<Date> {
let now = Date()
let calendar = Calendar.current
let startOfToday = calendar.startOfDay(for: now)
let start: Date = switch self {
case .today: startOfToday
case .week: calendar.date(byAdding: .day, value: -7, to: startOfToday)!
case .month: calendar.date(byAdding: .day, value: -30, to: startOfToday)!
case .threeMonths: calendar.date(byAdding: .month, value: -3, to: startOfToday)!
case .sixMonths: calendar.date(byAdding: .month, value: -6, to: startOfToday)!
}
return start...now
}
var fileCutoff: Date {
let calendar = Calendar.current
let startOfToday = calendar.startOfDay(for: Date())
return switch self {
case .today: calendar.date(byAdding: .day, value: -1, to: startOfToday)!
case .week: calendar.date(byAdding: .day, value: -8, to: startOfToday)!
case .month: calendar.date(byAdding: .day, value: -31, to: startOfToday)!
case .threeMonths: calendar.date(byAdding: .month, value: -3, to: calendar.date(byAdding: .day, value: -1, to: startOfToday)!)!
case .sixMonths: calendar.date(byAdding: .month, value: -6, to: calendar.date(byAdding: .day, value: -1, to: startOfToday)!)!
}
}
}

View file

@ -1,162 +0,0 @@
import Foundation
import SwiftUI
@MainActor
final class SessionStore: ObservableObject {
@Published var providers: [ProviderData] = []
@Published var selectedProvider: String = "all"
@Published var selectedPeriod: Period = .today
@Published var isLoading = false
@Published var grantedFolders: [URL] = []
@Published var needsOnboarding: Bool = true
var todayCost: Double {
providers.reduce(0) { $0 + $1.totalCost }
}
private var bookmarks: [Data] = []
private var refreshTimer: Timer?
enum Period: String, CaseIterable, Identifiable {
case today, week, month, threeMonths, sixMonths
var id: String { rawValue }
var label: String {
switch self {
case .today: "Today"
case .week: "7 Days"
case .month: "30 Days"
case .threeMonths: "3 Months"
case .sixMonths: "6 Months"
}
}
}
init() {
loadBookmarks()
needsOnboarding = grantedFolders.isEmpty
if !needsOnboarding {
Task { await refresh() }
startAutoRefresh()
}
}
nonisolated func stopTimer() {
MainActor.assumeIsolated {
refreshTimer?.invalidate()
}
}
func grantFolderAccess() {
let panel = NSOpenPanel()
panel.message = "Select your home folder so CodeBurn Pro can read session logs from coding tools."
panel.prompt = "Grant Access"
panel.canChooseDirectories = true
panel.canChooseFiles = false
panel.allowsMultipleSelection = false
panel.directoryURL = FileManager.default.homeDirectoryForCurrentUser
guard panel.runModal() == .OK, let url = panel.url else { return }
guard url.startAccessingSecurityScopedResource() else { return }
defer { url.stopAccessingSecurityScopedResource() }
if let bookmark = try? url.bookmarkData(
options: .withSecurityScope,
includingResourceValuesForKeys: nil,
relativeTo: nil
) {
bookmarks.append(bookmark)
grantedFolders.append(url)
saveBookmarks()
needsOnboarding = false
Task { await refresh() }
startAutoRefresh()
}
}
func refresh() async {
isLoading = true
let resolvedURLs = resolveBookmarks()
let period = selectedPeriod.dateRange
let fileCutoff = selectedPeriod.fileCutoff
NSLog("CodeBurnPro: refresh — %d bookmarks, period %@..%@", resolvedURLs.count, "\(period.lowerBound)", "\(period.upperBound)")
let result: [ProviderData] = await Task.detached(priority: .userInitiated) {
var allData: [ProviderData] = []
for url in resolvedURLs {
guard url.startAccessingSecurityScopedResource() else {
NSLog("CodeBurnPro: failed to access: %@", url.path)
continue
}
defer { url.stopAccessingSecurityScopedResource() }
let discovered = SessionDiscovery.discoverAll(under: url, modifiedAfter: fileCutoff)
for (providerName, paths) in discovered {
NSLog("CodeBurnPro: %@ — %d files", providerName, paths.count)
let parser = ParserFactory.parser(for: providerName)
let sessions = parser.parseSessions(at: paths, in: period)
NSLog("CodeBurnPro: %@ — %d sessions, $%.2f", providerName, sessions.count, sessions.reduce(0) { $0 + $1.cost })
if let idx = allData.firstIndex(where: { $0.name == providerName }) {
allData[idx].sessions.append(contentsOf: sessions)
} else {
allData.append(ProviderData(name: providerName, sessions: sessions))
}
}
}
return allData
}.value
providers = result
isLoading = false
}
private func startAutoRefresh() {
refreshTimer?.invalidate()
refreshTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in
guard let self else { return }
Task { @MainActor in
await self.refresh()
}
}
}
// MARK: - Bookmark persistence
private static var bookmarkURL: URL {
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("CodeBurnPro", isDirectory: true)
.appendingPathComponent("bookmarks.plist")
}
private func saveBookmarks() {
let dir = Self.bookmarkURL.deletingLastPathComponent()
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
try? PropertyListEncoder().encode(bookmarks).write(to: Self.bookmarkURL)
}
private func loadBookmarks() {
guard let data = try? Data(contentsOf: Self.bookmarkURL),
let decoded = try? PropertyListDecoder().decode([Data].self, from: data)
else { return }
bookmarks = decoded
grantedFolders = resolveBookmarks()
needsOnboarding = grantedFolders.isEmpty
}
private func resolveBookmarks() -> [URL] {
bookmarks.compactMap { data in
var isStale = false
guard let url = try? URL(
resolvingBookmarkData: data,
options: .withSecurityScope,
relativeTo: nil,
bookmarkDataIsStale: &isStale
) else { return nil }
return url
}
}
}

View file

@ -1,58 +0,0 @@
{
"images" : [
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,928 +0,0 @@
import SwiftUI
// MARK: - Shared Components
struct SectionCaption: View {
let text: String
var body: some View {
HStack(spacing: 5) {
Circle()
.fill(Theme.brandAccent.opacity(0.7))
.frame(width: 3, height: 3)
Text(text)
.font(.system(size: 11.5, weight: .medium))
.foregroundStyle(.secondary)
.tracking(-0.1)
}
}
}
struct CollapsibleSection<Trailing: View, Content: View>: View {
let caption: String
@Binding var isExpanded: Bool
let trailing: Trailing
let content: Content
init(
caption: String,
isExpanded: Binding<Bool>,
@ViewBuilder trailing: () -> Trailing,
@ViewBuilder content: () -> Content
) {
self.caption = caption
self._isExpanded = isExpanded
self.trailing = trailing()
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 7) {
Button {
withAnimation(.easeInOut(duration: 0.18)) { isExpanded.toggle() }
} label: {
HStack(spacing: 8) {
HStack(spacing: 5) {
Circle()
.fill(Theme.brandAccent.opacity(0.7))
.frame(width: 3, height: 3)
Text(caption)
.font(.system(size: 11.5, weight: .medium))
.tracking(-0.1)
}
Spacer()
trailing
Image(systemName: "chevron.right")
.font(.system(size: 9, weight: .semibold))
.rotationEffect(.degrees(isExpanded ? 90 : 0))
.opacity(0.55)
}
.foregroundStyle(.secondary)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
if isExpanded {
content.transition(.opacity)
}
}
.padding(.horizontal, 14)
.padding(.vertical, 11)
}
}
extension CollapsibleSection where Trailing == EmptyView {
init(caption: String, isExpanded: Binding<Bool>, @ViewBuilder content: () -> Content) {
self.init(caption: caption, isExpanded: isExpanded, trailing: { EmptyView() }, content: content)
}
}
struct FixedBar: View {
let fraction: Double
var body: some View {
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 2).fill(.secondary.opacity(0.15))
RoundedRectangle(cornerRadius: 2)
.fill(Theme.brandAccent)
.frame(width: max(0, min(geo.size.width, geo.size.width * CGFloat(fraction))))
}
}
}
}
private struct MiniStat: View {
let label: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: 1) {
Text(label)
.font(.system(size: 9.5, weight: .medium))
.foregroundStyle(.tertiary)
Text(value)
.font(.system(size: 11.5, weight: .semibold))
.monospacedDigit()
.foregroundStyle(.primary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
// MARK: - Insight Pill Switcher
enum InsightMode: String, CaseIterable, Identifiable {
case trend = "Trend"
case forecast = "Forecast"
case pulse = "Pulse"
case stats = "Stats"
var id: String { rawValue }
}
struct InsightSection: View {
@ObservedObject var store: SessionStore
@State private var selected: InsightMode = .trend
var body: some View {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 4) {
ForEach(InsightMode.allCases) { mode in
Button { selected = mode } label: {
Text(mode.rawValue)
.font(.system(size: 11, weight: .medium))
.foregroundStyle(selected == mode ? AnyShapeStyle(.white) : AnyShapeStyle(.secondary))
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(selected == mode ? AnyShapeStyle(Theme.brandAccent) : AnyShapeStyle(Color.secondary.opacity(0.10)))
)
}
.buttonStyle(.plain)
}
}
switch selected {
case .trend: TrendInsight(store: store)
case .forecast: ForecastInsight(store: store)
case .pulse: PulseInsight(store: store)
case .stats: StatsInsight(store: store)
}
}
.padding(.horizontal, 14)
.padding(.top, 10)
.padding(.bottom, 10)
}
}
// MARK: - Trend Insight
private let trendDays = 19
private let trendBarWidth: CGFloat = 13
private let trendBarGap: CGFloat = 4
private let trendChartHeight: CGFloat = 90
private struct TrendBar: Identifiable {
var id: String { date }
let date: String
let cost: Double
let inputTokens: Double
let outputTokens: Double
let isToday: Bool
let topModels: [(name: String, cost: Double)]
var tokens: Double { inputTokens + outputTokens }
}
private struct TrendInsight: View {
@ObservedObject var store: SessionStore
var body: some View {
let bars = buildBars()
let totalCost = bars.reduce(0.0) { $0 + $1.cost }
let totalTokens = bars.reduce(0.0) { $0 + $1.tokens }
let useTokens = totalTokens > 0
let metric: (TrendBar) -> Double = useTokens ? { $0.tokens } : { $0.cost }
let maxValue = max(bars.map(metric).max() ?? 1, 0.01)
let avgValue = bars.isEmpty ? 0 : bars.map(metric).reduce(0, +) / Double(bars.count)
let peakBar = bars.filter({ metric($0) > 0 }).max(by: { metric($0) < metric($1) })
let yesterdayBar = bars.dropLast().last
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .firstTextBaseline) {
VStack(alignment: .leading, spacing: 1) {
Text("Last \(trendDays) days")
.font(.system(size: 10, weight: .medium))
.foregroundStyle(.tertiary)
Text(useTokens ? "\(formatTokens(totalTokens)) tokens" : totalCost.asCurrency())
.font(.system(size: 18, weight: .semibold, design: .rounded))
.monospacedDigit()
.foregroundStyle(.primary)
}
Spacer()
if let delta = deltaPercent(bars: bars, metric: metric) {
HStack(spacing: 3) {
Image(systemName: delta >= 0 ? "arrow.up.right" : "arrow.down.right")
.font(.system(size: 9, weight: .bold))
Text("\(delta >= 0 ? "+" : "")\(String(format: "%.0f", delta))% vs prior \(trendDays)d")
.font(.system(size: 10.5))
.monospacedDigit()
}
.foregroundStyle(Theme.brandAccent)
}
}
TrendChart(bars: bars, maxValue: maxValue, avgValue: avgValue, metric: metric) { v in
useTokens ? "\(formatTokens(v)) tok" : v.asCompactCurrency()
}
HStack(spacing: 14) {
MiniStat(label: "Avg/day", value: formatValue(avgValue, useTokens: useTokens))
MiniStat(label: "Peak", value: peakLabel(peakBar, metric: metric, useTokens: useTokens))
MiniStat(label: "Yesterday", value: yesterdayBar.map { formatValue(metric($0), useTokens: useTokens) } ?? "")
}
}
}
private func formatValue(_ v: Double, useTokens: Bool) -> String {
useTokens ? "\(formatTokens(v)) tok" : v.asCompactCurrency()
}
private func peakLabel(_ peak: TrendBar?, metric: (TrendBar) -> Double, useTokens: Bool) -> String {
guard let peak, metric(peak) > 0 else { return "" }
let parts = peak.date.split(separator: "-")
let short = parts.count == 3 ? "\(parts[1])/\(parts[2])" : peak.date
return "\(formatValue(metric(peak), useTokens: useTokens)) on \(short)"
}
private func deltaPercent(bars: [TrendBar], metric: (TrendBar) -> Double) -> Double? {
let sessions = filteredSessions
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
guard let priorStart = calendar.date(byAdding: .day, value: -(2 * trendDays - 1), to: today),
let thisStart = calendar.date(byAdding: .day, value: -(trendDays - 1), to: today)
else { return nil }
let thisTotal = bars.map(metric).reduce(0, +)
var priorTotal = 0.0
for session in sessions {
let sd = calendar.startOfDay(for: session.startDate)
if sd >= priorStart && sd < thisStart {
priorTotal += session.cost
}
}
guard priorTotal > 0 else { return nil }
return ((thisTotal - priorTotal) / priorTotal) * 100
}
private func buildBars() -> [TrendBar] {
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let todayKey = formatter.string(from: today)
let sessions = filteredSessions
var costByDate: [String: Double] = [:]
var inputByDate: [String: Double] = [:]
var outputByDate: [String: Double] = [:]
var modelsByDate: [String: [String: Double]] = [:]
for session in sessions {
for turn in session.turns {
let key = formatter.string(from: turn.timestamp)
costByDate[key, default: 0] += turn.cost
inputByDate[key, default: 0] += Double(turn.inputTokens)
outputByDate[key, default: 0] += Double(turn.outputTokens)
modelsByDate[key, default: [:]][turn.model, default: 0] += turn.cost
}
if session.turns.isEmpty {
let key = formatter.string(from: session.startDate)
costByDate[key, default: 0] += session.cost
}
}
return (0..<trendDays).reversed().map { offset in
let date = calendar.date(byAdding: .day, value: -offset, to: today)!
let key = formatter.string(from: date)
let models = (modelsByDate[key] ?? [:]).sorted(by: { $0.value > $1.value }).prefix(3).map { ($0.key, $0.value) }
return TrendBar(
date: key,
cost: costByDate[key] ?? 0,
inputTokens: inputByDate[key] ?? 0,
outputTokens: outputByDate[key] ?? 0,
isToday: key == todayKey,
topModels: models
)
}
}
private var filteredSessions: [ParsedSession] {
if store.selectedProvider == "all" {
return store.providers.flatMap(\.sessions)
}
return store.providers.first(where: { $0.name == store.selectedProvider })?.sessions ?? []
}
}
private struct TrendChart: View {
let bars: [TrendBar]
let maxValue: Double
let avgValue: Double
let metric: (TrendBar) -> Double
let formatValue: (Double) -> String
@State private var hoveredBarID: TrendBar.ID?
var body: some View {
let avgFraction = maxValue > 0 ? CGFloat(min(avgValue / maxValue, 1.0)) : 0
ZStack(alignment: .bottomLeading) {
HStack(alignment: .bottom, spacing: trendBarGap) {
ForEach(bars) { bar in
BarColumn(bar: bar, value: metric(bar), maxValue: maxValue, isHovered: hoveredBarID == bar.id)
.onHover { hovering in
hoveredBarID = hovering ? bar.id : (hoveredBarID == bar.id ? nil : hoveredBarID)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: trendChartHeight, alignment: .bottom)
GeometryReader { geo in
Path { p in
let y = geo.size.height - (geo.size.height * avgFraction)
p.move(to: CGPoint(x: 0, y: y))
p.addLine(to: CGPoint(x: geo.size.width, y: y))
}
.stroke(Color.secondary.opacity(0.5), style: StrokeStyle(lineWidth: 1, dash: [3, 3]))
}
.frame(height: trendChartHeight)
.allowsHitTesting(false)
}
.frame(height: trendChartHeight)
.overlay(alignment: .bottomLeading) {
if let bar = bars.first(where: { $0.id == hoveredBarID }) {
BarTooltipCard(bar: bar, value: metric(bar), formatValue: formatValue)
.padding(.top, 6)
.offset(y: 92)
.transition(.opacity)
.allowsHitTesting(false)
.zIndex(10)
}
}
.animation(.easeInOut(duration: 0.12), value: hoveredBarID)
}
}
private struct BarColumn: View {
let bar: TrendBar
let value: Double
let maxValue: Double
let isHovered: Bool
var body: some View {
let fraction = maxValue > 0 ? CGFloat(value / maxValue) : 0
let height = max(2, trendChartHeight * fraction)
VStack(spacing: 2) {
Spacer(minLength: 0)
RoundedRectangle(cornerRadius: 2)
.fill(barColor)
.frame(width: trendBarWidth, height: height)
.overlay(
RoundedRectangle(cornerRadius: 2)
.stroke(Theme.brandAccent.opacity(isHovered ? 0.9 : 0), lineWidth: 1)
)
.scaleEffect(x: isHovered ? 1.08 : 1.0, y: 1.0, anchor: .bottom)
.animation(.easeOut(duration: 0.12), value: isHovered)
}
.contentShape(Rectangle())
}
private var barColor: Color {
if bar.isToday { return Theme.brandAccent }
if value <= 0 { return Color.secondary.opacity(0.15) }
return isHovered ? Theme.brandAccent.opacity(0.85) : Theme.brandAccent.opacity(0.55)
}
}
private struct BarTooltipCard: View {
let bar: TrendBar
let value: Double
let formatValue: (Double) -> String
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack(alignment: .leading, spacing: 5) {
HStack(alignment: .firstTextBaseline) {
Text(prettyDate(bar.date))
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(colorScheme == .dark ? .black : .white)
Spacer()
Text(formatValue(value))
.font(.codeMono(size: 10.5, weight: .semibold))
.foregroundStyle(Theme.brandAccent)
}
if !bar.topModels.isEmpty {
VStack(alignment: .leading, spacing: 3) {
ForEach(bar.topModels.prefix(3), id: \.name) { m in
HStack(spacing: 6) {
Circle().fill(Theme.brandAccent.opacity(0.7)).frame(width: 4, height: 4)
Text(m.name)
.font(.system(size: 10, weight: .medium))
.foregroundStyle(colorScheme == .dark ? .black : .white)
Spacer()
Text(m.cost.asCompactCurrency())
.font(.codeMono(size: 9.5, weight: .medium))
.foregroundStyle(colorScheme == .dark ? Color.black.opacity(0.7) : Color.white.opacity(0.72))
}
}
}
}
}
.padding(11)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(colorScheme == .dark ? Color.white : Color.black)
)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(colorScheme == .dark ? Color.black.opacity(0.12) : Color.white.opacity(0.12), lineWidth: 0.5)
)
.shadow(color: Color.black.opacity(0.35), radius: 10, y: 4)
}
}
// MARK: - Forecast Insight
private struct ForecastInsight: View {
@ObservedObject var store: SessionStore
var body: some View {
let stats = computeForecast()
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .firstTextBaseline) {
VStack(alignment: .leading, spacing: 2) {
Text("Month-to-date")
.font(.system(size: 10, weight: .medium))
.foregroundStyle(.tertiary)
Text(stats.mtd.asCurrency())
.font(.system(size: 22, weight: .semibold, design: .rounded))
.monospacedDigit()
.foregroundStyle(Theme.brandAccent)
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text("On pace for")
.font(.system(size: 10, weight: .medium))
.foregroundStyle(.tertiary)
Text(stats.projection.asCurrency())
.font(.system(size: 16, weight: .semibold))
.monospacedDigit()
}
}
HStack(spacing: 14) {
MiniStat(label: "Avg/day (this wk)", value: stats.weekAvg.asCompactCurrency())
MiniStat(label: "Yesterday", value: stats.yesterday.asCompactCurrency())
MiniStat(label: "Last 7d", value: stats.weekTotal.asCompactCurrency())
}
}
}
private func computeForecast() -> (mtd: Double, projection: Double, weekAvg: Double, weekTotal: Double, yesterday: Double) {
let calendar = Calendar.current
let now = Date()
let today = calendar.startOfDay(for: now)
let comps = calendar.dateComponents([.year, .month, .day], from: now)
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let sessions = filteredSessions
var costByDate: [String: Double] = [:]
for session in sessions {
let key = formatter.string(from: session.startDate)
costByDate[key, default: 0] += session.cost
}
let firstOfMonth = calendar.date(from: DateComponents(year: comps.year, month: comps.month, day: 1))!
let firstStr = formatter.string(from: firstOfMonth)
let daysInMonth = calendar.range(of: .day, in: .month, for: firstOfMonth)?.count ?? 30
let dayOfMonth = comps.day ?? 1
let mtd = costByDate.filter { $0.key >= firstStr }.values.reduce(0, +)
let avgPerDay = dayOfMonth > 0 ? mtd / Double(dayOfMonth) : 0
let projection = avgPerDay * Double(daysInMonth)
let weekStart = calendar.date(byAdding: .day, value: -6, to: today)!
let weekStartStr = formatter.string(from: weekStart)
let weekTotal = costByDate.filter { $0.key >= weekStartStr }.values.reduce(0, +)
let weekAvg = weekTotal / 7.0
let yesterdayStr = formatter.string(from: calendar.date(byAdding: .day, value: -1, to: today)!)
let yesterday = costByDate[yesterdayStr] ?? 0
return (mtd, projection, weekAvg, weekTotal, yesterday)
}
private var filteredSessions: [ParsedSession] {
if store.selectedProvider == "all" {
return store.providers.flatMap(\.sessions)
}
return store.providers.first(where: { $0.name == store.selectedProvider })?.sessions ?? []
}
}
// MARK: - Pulse Insight
private struct PulseInsight: View {
@ObservedObject var store: SessionStore
var body: some View {
let sessions = filteredSessions
let totalInput = sessions.reduce(0) { $0 + $1.inputTokens }
let totalCacheRead = sessions.reduce(0) { $0 + $1.cacheReadTokens }
let cacheHit = totalInput > 0 ? Double(totalCacheRead) / Double(totalInput + totalCacheRead) * 100 : 0
let costPerSession = sessions.isEmpty ? 0 : sessions.reduce(0.0) { $0 + $1.cost } / Double(sessions.count)
HStack(spacing: 10) {
PulseTile(label: "Cache hit", value: totalCacheRead > 0 ? String(format: "%.0f%%", cacheHit) : "", color: Theme.brandAccent)
PulseTile(label: "Cost / session", value: sessions.isEmpty ? "" : costPerSession.asCompactCurrency(), color: .secondary)
PulseTile(label: "Sessions", value: "\(sessions.count)", color: Theme.brandAccent)
}
}
private var filteredSessions: [ParsedSession] {
if store.selectedProvider == "all" {
return store.providers.flatMap(\.sessions)
}
return store.providers.first(where: { $0.name == store.selectedProvider })?.sessions ?? []
}
}
private struct PulseTile: View {
let label: String
let value: String
let color: Color
var body: some View {
VStack(alignment: .leading, spacing: 3) {
Text(label)
.font(.system(size: 10, weight: .medium))
.foregroundStyle(.tertiary)
Text(value)
.font(.system(size: 18, weight: .semibold, design: .rounded))
.monospacedDigit()
.foregroundStyle(color)
}
.padding(.horizontal, 10)
.padding(.vertical, 8)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(Color.secondary.opacity(0.06))
)
}
}
// MARK: - Stats Insight
private struct StatsInsight: View {
@ObservedObject var store: SessionStore
var body: some View {
let stats = computeStats()
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .top, spacing: 14) {
VStack(alignment: .leading, spacing: 8) {
StatRow(label: "Favorite model", value: stats.favoriteModel)
StatRow(label: "Active days", value: stats.activeDaysFraction)
StatRow(label: "Peak day spend", value: stats.peakDaySpend)
}
.frame(maxWidth: .infinity, alignment: .leading)
VStack(alignment: .leading, spacing: 8) {
StatRow(label: "Sessions today", value: "\(stats.sessionsToday)")
StatRow(label: "Current streak", value: stats.currentStreak)
StatRow(label: "Longest streak", value: stats.longestStreak)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
if stats.lifetimeTotal > 0 {
Divider().opacity(0.5)
HStack {
Text("Total tracked spend")
.font(.system(size: 10.5, weight: .medium))
.foregroundStyle(.tertiary)
Spacer()
Text(stats.lifetimeTotal.asCurrency())
.font(.system(size: 13, weight: .semibold, design: .rounded))
.monospacedDigit()
.foregroundStyle(Theme.brandAccent)
}
}
}
}
private struct Stats {
let favoriteModel: String
let activeDaysFraction: String
let peakDaySpend: String
let sessionsToday: Int
let currentStreak: String
let longestStreak: String
let lifetimeTotal: Double
}
private func computeStats() -> Stats {
let sessions = filteredSessions
let allTurns = sessions.flatMap(\.turns)
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let modelCosts = Dictionary(grouping: allTurns, by: \.model)
.mapValues { $0.reduce(0.0) { $0 + $1.cost } }
let favoriteModel = modelCosts.max(by: { $0.value < $1.value })?.key ?? ""
var costByDate: [String: Double] = [:]
for session in sessions {
let key = formatter.string(from: session.startDate)
costByDate[key, default: 0] += session.cost
}
let comps = calendar.dateComponents([.year, .month, .day], from: Date())
let firstOfMonth = calendar.date(from: DateComponents(year: comps.year, month: comps.month, day: 1))!
let firstStr = formatter.string(from: firstOfMonth)
let daysInMonth = calendar.range(of: .day, in: .month, for: firstOfMonth)?.count ?? 30
let activeDays = costByDate.filter { $0.key >= firstStr && $0.value > 0 }.count
let peak = costByDate.max(by: { $0.value < $1.value })
let peakDaySpend = peak.map { $0.value.asCompactCurrency() } ?? ""
let todayStr = formatter.string(from: today)
let sessionsToday = sessions.filter { formatter.string(from: $0.startDate) == todayStr }.count
var currentStreak = 0
for offset in 0..<400 {
guard let d = calendar.date(byAdding: .day, value: -offset, to: today) else { break }
let key = formatter.string(from: d)
if (costByDate[key] ?? 0) > 0 { currentStreak += 1 } else { break }
}
var longestStreak = 0
var running = 0
let sortedDates = costByDate.keys.sorted()
if let first = sortedDates.first, let last = sortedDates.last,
let start = formatter.date(from: first), let end = formatter.date(from: last) {
var cursor = start
while cursor <= end {
let key = formatter.string(from: cursor)
if (costByDate[key] ?? 0) > 0 {
running += 1
longestStreak = max(longestStreak, running)
} else {
running = 0
}
guard let next = calendar.date(byAdding: .day, value: 1, to: cursor) else { break }
cursor = next
}
}
let lifetime = sessions.reduce(0.0) { $0 + $1.cost }
return Stats(
favoriteModel: favoriteModel,
activeDaysFraction: "\(activeDays)/\(daysInMonth)",
peakDaySpend: peakDaySpend,
sessionsToday: sessionsToday,
currentStreak: currentStreak == 0 ? "" : "\(currentStreak) days",
longestStreak: longestStreak == 0 ? "" : "\(longestStreak) days",
lifetimeTotal: lifetime
)
}
private var filteredSessions: [ParsedSession] {
if store.selectedProvider == "all" {
return store.providers.flatMap(\.sessions)
}
return store.providers.first(where: { $0.name == store.selectedProvider })?.sessions ?? []
}
}
private struct StatRow: View {
let label: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: 1) {
Text(label)
.font(.system(size: 9.5, weight: .medium))
.foregroundStyle(.tertiary)
Text(value)
.font(.system(size: 12, weight: .semibold))
.monospacedDigit()
.foregroundStyle(.primary)
}
}
}
// MARK: - Activity Section (Projects)
struct ActivitySection: View {
@ObservedObject var store: SessionStore
@State private var isExpanded: Bool = true
var body: some View {
let projects = topProjects
if projects.isEmpty { return AnyView(EmptyView()) }
let maxCost = projects.first?.cost ?? 1
return AnyView(
CollapsibleSection(
caption: "Activity",
isExpanded: $isExpanded,
trailing: {
HStack(spacing: 8) {
Text("Cost").frame(minWidth: 54, alignment: .trailing)
Text("Turns").frame(minWidth: 52, alignment: .trailing)
}
.font(.system(size: 10, weight: .medium))
.foregroundStyle(.tertiary)
.tracking(-0.05)
}
) {
VStack(alignment: .leading, spacing: 7) {
ForEach(projects, id: \.name) { project in
HStack(spacing: 8) {
FixedBar(fraction: project.cost / maxCost)
.frame(width: 56, height: 6)
Text(project.name)
.font(.system(size: 12.5, weight: .medium))
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.truncationMode(.middle)
Text(project.cost.asCompactCurrency())
.font(.codeMono(size: 12, weight: .medium))
.tracking(-0.2)
.frame(minWidth: 54, alignment: .trailing)
Text("\(project.turns)")
.font(.system(size: 11))
.monospacedDigit()
.foregroundStyle(.secondary)
.frame(minWidth: 52, alignment: .trailing)
}
.padding(.vertical, 1)
}
}
}
)
}
private struct ProjectEntry {
let name: String
let cost: Double
let turns: Int
}
private var topProjects: [ProjectEntry] {
let sessions = filteredSessions
var byCost: [String: Double] = [:]
var byTurns: [String: Int] = [:]
for s in sessions {
byCost[s.project, default: 0] += s.cost
byTurns[s.project, default: 0] += s.turns.count
}
return byCost.map { ProjectEntry(name: $0.key, cost: $0.value, turns: byTurns[$0.key] ?? 0) }
.sorted(by: { $0.cost > $1.cost })
.prefix(8)
.map { $0 }
}
private var filteredSessions: [ParsedSession] {
if store.selectedProvider == "all" {
return store.providers.flatMap(\.sessions)
}
return store.providers.first(where: { $0.name == store.selectedProvider })?.sessions ?? []
}
}
// MARK: - Models Section
struct ModelsSection: View {
@ObservedObject var store: SessionStore
@State private var isExpanded: Bool = true
var body: some View {
let models = topModels
if models.isEmpty { return AnyView(EmptyView()) }
let maxCost = models.first?.cost ?? 1
return AnyView(
CollapsibleSection(
caption: "Models",
isExpanded: $isExpanded,
trailing: {
HStack(spacing: 8) {
Text("Cost").frame(minWidth: 54, alignment: .trailing)
Text("Calls").frame(minWidth: 52, alignment: .trailing)
}
.font(.system(size: 10, weight: .medium))
.foregroundStyle(.tertiary)
.tracking(-0.05)
}
) {
VStack(alignment: .leading, spacing: 7) {
ForEach(models, id: \.name) { model in
HStack(spacing: 8) {
FixedBar(fraction: model.cost / maxCost)
.frame(width: 56, height: 6)
Text(model.name)
.font(.system(size: 12.5, weight: .medium))
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
Text(model.cost.asCompactCurrency())
.font(.codeMono(size: 12, weight: .medium))
.tracking(-0.2)
.frame(minWidth: 54, alignment: .trailing)
Text("\(model.calls)")
.font(.system(size: 11))
.monospacedDigit()
.foregroundStyle(.secondary)
.frame(minWidth: 52, alignment: .trailing)
}
.padding(.vertical, 1)
}
TokensLine(store: store)
.padding(.top, 5)
}
}
)
}
private struct ModelEntry {
let name: String
let cost: Double
let calls: Int
}
private var topModels: [ModelEntry] {
let sessions = filteredSessions
let allTurns = sessions.flatMap(\.turns)
var byCost: [String: Double] = [:]
var byCalls: [String: Int] = [:]
for t in allTurns {
byCost[t.model, default: 0] += t.cost
byCalls[t.model, default: 0] += 1
}
return byCost.map { ModelEntry(name: $0.key, cost: $0.value, calls: byCalls[$0.key] ?? 0) }
.sorted(by: { $0.cost > $1.cost })
.prefix(6)
.map { $0 }
}
private var filteredSessions: [ParsedSession] {
if store.selectedProvider == "all" {
return store.providers.flatMap(\.sessions)
}
return store.providers.first(where: { $0.name == store.selectedProvider })?.sessions ?? []
}
}
private struct TokensLine: View {
@ObservedObject var store: SessionStore
var body: some View {
let sessions = store.selectedProvider == "all"
? store.providers.flatMap(\.sessions)
: store.providers.first(where: { $0.name == store.selectedProvider })?.sessions ?? []
let totalIn = sessions.reduce(0) { $0 + $1.inputTokens }
let totalOut = sessions.reduce(0) { $0 + $1.outputTokens }
let totalCacheRead = sessions.reduce(0) { $0 + $1.cacheReadTokens }
let cacheHit = (totalIn + totalCacheRead) > 0
? String(format: "%.0f", Double(totalCacheRead) / Double(totalIn + totalCacheRead) * 100)
: "0"
HStack(spacing: 4) {
Text("Tokens")
.foregroundStyle(.tertiary)
Text(formatTokens(totalIn) + " in")
.foregroundStyle(.secondary)
Text("·")
.foregroundStyle(.tertiary)
Text(formatTokens(totalOut) + " out")
.foregroundStyle(.secondary)
Text("·")
.foregroundStyle(.tertiary)
Text(cacheHit + "% cache hit")
.foregroundStyle(.secondary)
Spacer()
}
.font(.system(size: 10.5))
.monospacedDigit()
}
}
// MARK: - Helpers
private func prettyDate(_ ymd: String) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
guard let date = formatter.date(from: ymd) else { return ymd }
let pretty = DateFormatter()
pretty.dateFormat = "EEE MMM d"
return pretty.string(from: date)
}
func formatTokens(_ n: Double) -> String {
if n >= 1_000_000 { return String(format: "%.1fM", n / 1_000_000) }
if n >= 1_000 { return String(format: "%.0fK", n / 1_000) }
return String(format: "%.0f", n)
}
func formatTokens(_ n: Int) -> String {
formatTokens(Double(n))
}

View file

@ -1,404 +0,0 @@
import SwiftUI
struct MenuBarPopover: View {
@ObservedObject var store: SessionStore
var body: some View {
if store.needsOnboarding {
OnboardingView(store: store)
.frame(width: 360, height: 300)
} else {
DashboardView(store: store)
.frame(width: 380)
}
}
}
// MARK: - Dashboard
struct DashboardView: View {
@ObservedObject var store: SessionStore
@State private var showSettings = false
private var totalCost: Double {
filteredSessions.reduce(0) { $0 + $1.cost }
}
private var totalCalls: Int {
filteredSessions.reduce(0) { $0 + $1.calls }
}
private var totalSessions: Int {
filteredSessions.count
}
private var filteredSessions: [ParsedSession] {
if store.selectedProvider == "all" {
return store.providers.flatMap(\.sessions)
}
return store.providers.first(where: { $0.name == store.selectedProvider })?.sessions ?? []
}
var body: some View {
VStack(spacing: 0) {
Header(store: store)
Divider()
if !store.providers.isEmpty {
AgentTabStrip(store: store)
Divider()
}
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
HeroSection(store: store, cost: totalCost, calls: totalCalls, sessions: totalSessions)
Divider().opacity(0.5)
PeriodSegmentedControl(store: store)
Divider().opacity(0.5)
if isFilteredEmpty {
EmptyProviderState(provider: store.selectedProvider, period: store.selectedPeriod)
} else {
InsightSection(store: store)
.zIndex(10)
Divider().opacity(0.5)
ActivitySection(store: store)
Divider().opacity(0.5)
ModelsSection(store: store)
}
}
}
.frame(height: 520)
.animation(.easeInOut(duration: 0.2), value: store.isLoading)
Divider()
FooterBar(store: store, showSettings: $showSettings)
}
}
private var isFilteredEmpty: Bool {
totalCost <= 0 && totalCalls <= 0
}
}
// MARK: - Header
private struct Header: View {
@ObservedObject var store: SessionStore
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 1) {
(
Text("Code").foregroundStyle(.primary)
+ Text("Burn").foregroundStyle(Theme.brandEmber)
)
.font(.system(size: 13, weight: .semibold))
.tracking(-0.15)
Text("AI Coding Cost Tracker")
.font(.system(size: 10.5))
.foregroundStyle(.secondary)
}
Spacer()
if store.isLoading {
ProgressView()
.scaleEffect(0.6)
}
}
.padding(.horizontal, 14)
.padding(.top, 10)
.padding(.bottom, 8)
}
}
// MARK: - Hero Section
private struct HeroSection: View {
@ObservedObject var store: SessionStore
let cost: Double
let calls: Int
let sessions: Int
var body: some View {
VStack(alignment: .leading, spacing: 8) {
SectionCaption(text: caption)
HStack(alignment: .firstTextBaseline) {
Text(cost.asCurrency())
.font(.system(size: 32, weight: .semibold, design: .rounded))
.monospacedDigit()
.tracking(-1)
.foregroundStyle(
LinearGradient(
colors: [Theme.brandAccent, Theme.brandAccentDeep],
startPoint: .top,
endPoint: .bottom
)
)
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text("\(calls.asThousandsSeparated()) calls")
.font(.system(size: 11))
.monospacedDigit()
.foregroundStyle(.secondary)
Text("\(sessions) sessions")
.font(.system(size: 10.5))
.monospacedDigit()
.foregroundStyle(.tertiary)
}
}
}
.padding(.horizontal, 14)
.padding(.top, 10)
.padding(.bottom, 12)
}
private var caption: String {
let label = store.selectedPeriod.label
if store.selectedPeriod == .today {
let formatter = DateFormatter()
formatter.dateFormat = "EEE MMM d"
return "\(label) · \(formatter.string(from: Date()))"
}
return label
}
}
// MARK: - Period Picker
private struct PeriodSegmentedControl: View {
@ObservedObject var store: SessionStore
var body: some View {
HStack(spacing: 1) {
ForEach(SessionStore.Period.allCases) { period in
Button {
store.selectedPeriod = period
Task { await store.refresh() }
} label: {
Text(period.label)
.font(.system(size: 11, weight: .medium))
.foregroundStyle(store.selectedPeriod == period ? AnyShapeStyle(.primary) : AnyShapeStyle(.secondary))
.frame(maxWidth: .infinity)
.padding(.vertical, 4)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.background(
RoundedRectangle(cornerRadius: 5)
.fill(store.selectedPeriod == period ? Color(NSColor.windowBackgroundColor).opacity(0.85) : .clear)
.shadow(color: .black.opacity(store.selectedPeriod == period ? 0.06 : 0), radius: 1, y: 0.5)
)
}
}
.padding(2)
.background(
RoundedRectangle(cornerRadius: 7)
.fill(Color.secondary.opacity(0.08))
)
.padding(.horizontal, 12)
.padding(.top, 6)
.padding(.bottom, 10)
}
}
// MARK: - Agent Tab Strip
private struct AgentTabStrip: View {
@ObservedObject var store: SessionStore
private var totalCost: Double {
store.providers.reduce(0) { $0 + $1.totalCost }
}
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 5) {
AgentTab(
name: "All",
cost: totalCost,
color: Theme.brandAccent,
isActive: store.selectedProvider == "all"
) {
store.selectedProvider = "all"
}
ForEach(store.providers) { provider in
AgentTab(
name: provider.name.capitalized,
cost: provider.totalCost,
color: Theme.providerColor(provider.name),
isActive: store.selectedProvider == provider.name
) {
store.selectedProvider = provider.name
}
}
}
.padding(.horizontal, 12)
.padding(.top, 8)
.padding(.bottom, 4)
}
.frame(height: 38)
}
}
private struct AgentTab: View {
let name: String
let cost: Double
let color: Color
let isActive: Bool
let onTap: () -> Void
var body: some View {
VStack(spacing: 3) {
HStack(spacing: 5) {
Text(name)
.font(.system(size: 11.5, weight: .medium))
.tracking(-0.05)
if cost > 0 {
Text(cost.asCompactCurrency())
.font(.codeMono(size: 10.5, weight: .medium))
.foregroundStyle(isActive ? AnyShapeStyle(.white.opacity(0.8)) : AnyShapeStyle(.secondary))
.tracking(-0.2)
}
}
}
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(isActive ? AnyShapeStyle(color) : AnyShapeStyle(Color.secondary.opacity(0.08)))
)
.foregroundStyle(isActive ? AnyShapeStyle(.white) : AnyShapeStyle(.secondary))
.contentShape(Rectangle())
.onTapGesture { onTap() }
}
}
// MARK: - Empty State
private struct EmptyProviderState: View {
let provider: String
let period: SessionStore.Period
var body: some View {
VStack(spacing: 10) {
Image(systemName: "tray")
.font(.system(size: 26))
.foregroundStyle(.tertiary)
Text("No \(provider == "all" ? "" : provider + " ")data for \(periodPhrase)")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 60)
}
private var periodPhrase: String {
switch period {
case .today: "today"
case .week: "the last 7 days"
case .month: "the last 30 days"
case .threeMonths: "the last 3 months"
case .sixMonths: "the last 6 months"
}
}
}
// MARK: - Footer
private struct FooterBar: View {
@ObservedObject var store: SessionStore
@Binding var showSettings: Bool
var body: some View {
HStack(spacing: 6) {
Button {
Task { await store.refresh() }
} label: {
Image(systemName: store.isLoading ? "arrow.triangle.2.circlepath" : "arrow.clockwise")
.font(.system(size: 11, weight: .medium))
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(store.isLoading)
Button {
showSettings = true
} label: {
Image(systemName: "gearshape")
.font(.system(size: 11, weight: .medium))
}
.buttonStyle(.bordered)
.controlSize(.small)
.popover(isPresented: $showSettings) {
SettingsView(store: store)
}
Spacer()
Text(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "")
.font(.system(size: 10, weight: .regular, design: .monospaced))
.foregroundStyle(.tertiary)
Button {
NSApplication.shared.terminate(nil)
} label: {
Text("Quit")
.font(.system(size: 11, weight: .medium))
}
.buttonStyle(.bordered)
.controlSize(.small)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
}
}
// MARK: - Onboarding
struct OnboardingView: View {
@ObservedObject var store: SessionStore
var body: some View {
VStack(spacing: 16) {
Image(systemName: "flame.fill")
.font(.system(size: 48))
.foregroundStyle(
LinearGradient(
colors: [Theme.brandAccent, Theme.brandAccentDeep],
startPoint: .top,
endPoint: .bottom
)
)
(
Text("Code").foregroundStyle(.primary)
+ Text("Burn").foregroundStyle(Theme.brandAccent)
+ Text(" Pro").foregroundStyle(.primary)
)
.font(.system(size: 18, weight: .semibold))
Text("Grant access to your home folder so CodeBurn can discover session logs from your coding tools.")
.font(.system(size: 12))
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
.padding(.horizontal)
Button("Grant Folder Access") {
store.grantFolderAccess()
}
.buttonStyle(.borderedProminent)
.tint(Theme.brandAccent)
.controlSize(.large)
Text("We only read session logs. Nothing is uploaded.")
.font(.system(size: 10))
.foregroundStyle(.tertiary)
}
.padding(24)
}
}

View file

@ -1,29 +0,0 @@
import SwiftUI
struct SettingsView: View {
@ObservedObject var store: SessionStore
var body: some View {
Form {
Section("Folder Access") {
ForEach(store.grantedFolders, id: \.absoluteString) { url in
HStack {
Image(systemName: "folder")
Text(url.path.replacingOccurrences(of: FileManager.default.homeDirectoryForCurrentUser.path, with: "~"))
}
}
Button("Add Folder...") {
store.grantFolderAccess()
}
}
Section("About") {
LabeledContent("Version", value: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "")
LabeledContent("Providers Found", value: "\(store.providers.count)")
}
}
.formStyle(.grouped)
.frame(width: 400, height: 300)
}
}

View file

@ -1,53 +0,0 @@
import SwiftUI
enum Theme {
static let brandEmber = Color(red: 0xC9/255.0, green: 0x52/255.0, blue: 0x1D/255.0)
static let brandAccent = Color(red: 0xC9/255.0, green: 0x52/255.0, blue: 0x1D/255.0)
static let brandAccentLight = Color(red: 0xE8/255.0, green: 0x77/255.0, blue: 0x4A/255.0)
static let brandAccentDeep = Color(red: 0x8B/255.0, green: 0x3E/255.0, blue: 0x13/255.0)
static let brandAccentGlow = Color(red: 0xF0/255.0, green: 0xA0/255.0, blue: 0x70/255.0)
static func providerColor(_ name: String) -> Color {
switch name.lowercased() {
case "claude": Color(red: 0xC9/255.0, green: 0x52/255.0, blue: 0x1D/255.0)
case "codex": Color(red: 0x4A/255.0, green: 0x7D/255.0, blue: 0x5C/255.0)
case "copilot": Color(red: 0x6D/255.0, green: 0x8F/255.0, blue: 0xA6/255.0)
case "cursor": Color(red: 0x3F/255.0, green: 0x6B/255.0, blue: 0x8C/255.0)
case "gemini": Color(red: 0x44/255.0, green: 0x85/255.0, blue: 0xF4/255.0)
case "cline": Color(red: 0x23/255.0, green: 0x8A/255.0, blue: 0x7E/255.0)
case "roo-code", "roo_code": Color(red: 0x4C/255.0, green: 0xAF/255.0, blue: 0x50/255.0)
case "kilo-code": Color(red: 0x00/255.0, green: 0x96/255.0, blue: 0x88/255.0)
default: .secondary
}
}
}
extension Font {
static func codeMono(size: CGFloat, weight: Font.Weight = .regular) -> Font {
.system(size: size, weight: weight, design: .monospaced)
}
}
extension Double {
func asCurrency() -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = "USD"
return formatter.string(from: NSNumber(value: self)) ?? "$0.00"
}
func asCompactCurrency() -> String {
if self >= 1000 { return String(format: "$%.0fK", self / 1000) }
if self >= 100 { return String(format: "$%.0f", self) }
if self >= 10 { return String(format: "$%.1f", self) }
return String(format: "$%.2f", self)
}
}
extension Int {
func asThousandsSeparated() -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter.string(from: NSNumber(value: self)) ?? "\(self)"
}
}

View file

@ -1,47 +0,0 @@
name: CodeBurnPro
options:
bundleIdPrefix: com.agentseal
deploymentTarget:
macOS: "14.0"
xcodeVersion: "16.0"
generateEmptyDirectories: true
settings:
base:
MARKETING_VERSION: "1.0.0"
CURRENT_PROJECT_VERSION: "1"
SWIFT_VERSION: "6.0"
MACOSX_DEPLOYMENT_TARGET: "14.0"
targets:
CodeBurnPro:
type: application
platform: macOS
sources:
- path: Sources
resources:
- path: Sources/Resources
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.agentseal.codeburn-pro
PRODUCT_NAME: CodeBurn Pro
CODE_SIGN_ENTITLEMENTS: Sources/App/CodeBurnPro.entitlements
INFOPLIST_FILE: Sources/App/Info.plist
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
ENABLE_HARDENED_RUNTIME: true
CODE_SIGN_IDENTITY: "Apple Development: toruk_makto1406@icloud.com (T3289GPL2P)"
CODE_SIGN_STYLE: Manual
DEVELOPMENT_TEAM: ""
entitlements:
path: Sources/App/CodeBurnPro.entitlements
CodeBurnProTests:
type: bundle.unit-test
platform: macOS
sources:
- path: Tests
dependencies:
- target: CodeBurnPro
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.agentseal.codeburn-pro-tests