goose2 distribution bundling (#8911)

This commit is contained in:
Jack Amadeo
2026-05-05 11:20:49 -04:00
committed by GitHub
parent fbb5e3685d
commit 9f3fe88afa
25 changed files with 589 additions and 29 deletions
+5 -11
View File
@@ -151,14 +151,10 @@ fn system_config_path() -> PathBuf {
}
}
fn bundled_defaults_path() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
let path = exe.parent()?.join("defaults.yaml");
if path.exists() {
Some(path)
} else {
None
}
fn additional_config_paths_from_env() -> Vec<PathBuf> {
env::var_os("GOOSE_ADDITIONAL_CONFIG_FILES")
.map(|value| env::split_paths(&value).collect())
.unwrap_or_default()
}
impl Default for Config {
@@ -167,9 +163,7 @@ impl Default for Config {
let user_config_path = config_dir.join(CONFIG_YAML_NAME);
let mut config_paths = vec![system_config_path()];
if let Some(defaults) = bundled_defaults_path() {
config_paths.insert(0, defaults);
}
config_paths.extend(additional_config_paths_from_env());
config_paths.push(user_config_path.clone());
let no_secrets_config = Self {
+1
View File
@@ -130,6 +130,7 @@ ThemeProvider manages three axes:
- Title bar uses `titleBarStyle: "Overlay"` with `hiddenTitle: true` for a custom titlebar.
- `tauri-plugin-window-state` persists window size and position.
- Traffic light offset: `pl-20` (80px) to accommodate macOS window controls.
- Distro bundle behavior, including feature flags, is documented in `distro/README.md`.
## Architecture
+101
View File
@@ -0,0 +1,101 @@
# Goose2 distro bundles
A Goose2 distro bundle is an optional app-specific package of configuration and policy that the Tauri shell loads at startup.
## What a distro bundle is
A distro bundle lives under `ui/goose2/distro/` in development, and is bundled into the packaged app as a Tauri resource in production.
Current supported files:
- `distro.json` — distro manifest
- `config.yaml` — optional Goose config passed to `goose serve`
- `bin/` — optional executables or helper scripts prepended to `PATH` for `goose serve`
## How it is discovered
The Tauri app resolves the distro bundle in this order:
1. `GOOSE_DISTRO_DIR`, if set
2. bundled Tauri resource dir at `resource_dir()/distro`
In development, `just dev` and `just dev-debug` automatically export `GOOSE_DISTRO_DIR` to `ui/goose2/distro` when that directory exists.
## Manifest shape
Example:
```json
{
"appVersion": "development",
"featureToggles": {
"costTracking": false
},
"providerAllowlist": "databricks"
}
```
### Fields
- `appVersion?: string`
- optional app version tag supplied by the distro
- `featureToggles?: Record<string, boolean>`
- optional UI/product flags controlled by the distro
- currently supported:
- `costTracking`
- `false` hides cost UI in the token/context usage surfaces
- omitted behaves as enabled
- `providerAllowlist?: string`
- comma-separated provider ids
- suggests which model providers to show in Settings
- suggests which Goose model options to show in the chat model picker
- `extensionAllowlist?: string`
- comma-separated extension ids
- reserved for future UI suggestions
## Runtime effects
When a distro bundle is present, Goose2 does two kinds of things with it.
### Frontend behavior
The frontend loads `get_distro_bundle` during app startup and stores the manifest in Zustand.
Today it uses that manifest to:
- filter model providers shown in provider settings via `providerAllowlist`
- filter Goose model options shown in the chat input model picker via `providerAllowlist`
- hide cost UI when `featureToggles.costTracking === false`
These allowlists are UI suggestions only. They do not enforce backend access control and do not invalidate existing sessions or saved model choices.
### Backend / shell behavior
When the Tauri shell launches the long-lived `goose serve` process, it applies the distro bundle like this:
- prepends `distro/bin` to `PATH` when present
- adds `distro/config.yaml` to `GOOSE_ADDITIONAL_CONFIG_FILES` when present
- sets `GOOSE_DISTRO_DIR` to the resolved distro root
This is shell-level behavior, so it is implemented as Tauri-side setup rather than an ACP method.
## Development notes
- packaged apps discover distro content from bundled Tauri resources
- local development uses `GOOSE_DISTRO_DIR`
- after changing `distro.json`, restart `just dev` so startup reloads the manifest
## Scope guidance
Use distro bundles for packaged-app policy and shell-level defaults.
Good fits:
- feature flags for Goose2 UI behavior
- allowlists that suggest visible product choices
- config or helper binaries that should be present when `goose serve` starts
Avoid using distro bundles as a replacement for normal app state, user settings, or ACP-backed domain data.
View File
+5
View File
@@ -0,0 +1,5 @@
{
"featureToggles": {
"costTracking": true
}
}
+16
View File
@@ -90,6 +90,10 @@ dev: setup
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
PROJECT_DIR=$(pwd)
REPO_ROOT=$(cd ../.. && pwd)
DISTRO_DIR="${PROJECT_DIR}/distro"
if [[ -z "${GOOSE_DISTRO_DIR:-}" && -d "${DISTRO_DIR}" ]]; then
export GOOSE_DISTRO_DIR="${DISTRO_DIR}"
fi
LOCAL_GOOSE_DEBUG="${REPO_ROOT}/target/debug/goose"
LOCAL_GOOSE_RELEASE="${REPO_ROOT}/target/release/goose"
if [[ -x "${LOCAL_GOOSE_DEBUG}" ]]; then
@@ -107,6 +111,10 @@ dev: setup
echo "No local goose binary found under ${REPO_ROOT}/target; falling back to PATH"
fi
if [[ -n "${GOOSE_DISTRO_DIR:-}" ]]; then
echo "Using distro dir: ${GOOSE_DISTRO_DIR}"
fi
# In worktrees, generate a labeled icon so you can tell instances apart
if git rev-parse --is-inside-work-tree &>/dev/null; then
GIT_DIR=$(git rev-parse --git-dir)
@@ -138,6 +146,10 @@ dev-debug: setup
# Override with e.g. RUST_LOG=info just dev-debug to disable.
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
REPO_ROOT=$(cd ../.. && pwd)
DISTRO_DIR="$(pwd)/distro"
if [[ -z "${GOOSE_DISTRO_DIR:-}" && -d "${DISTRO_DIR}" ]]; then
export GOOSE_DISTRO_DIR="${DISTRO_DIR}"
fi
LOCAL_GOOSE_DEBUG="${REPO_ROOT}/target/debug/goose"
LOCAL_GOOSE_RELEASE="${REPO_ROOT}/target/release/goose"
if [[ -x "${LOCAL_GOOSE_DEBUG}" ]]; then
@@ -155,6 +167,10 @@ dev-debug: setup
echo "No local goose binary found under ${REPO_ROOT}/target; falling back to PATH"
fi
if [[ -n "${GOOSE_DISTRO_DIR:-}" ]]; then
echo "Using distro dir: ${GOOSE_DISTRO_DIR}"
fi
# In worktrees, generate a labeled icon so you can tell instances apart
if git rev-parse --is-inside-work-tree &>/dev/null; then
GIT_DIR=$(git rev-parse --git-dir)
@@ -0,0 +1,7 @@
use crate::services::distro_bundle::{DistroBundleInfo, DistroBundleState};
use tauri::State;
#[tauri::command]
pub fn get_distro_bundle(state: State<'_, DistroBundleState>) -> DistroBundleInfo {
state.info()
}
+1
View File
@@ -1,6 +1,7 @@
pub mod acp;
pub mod agent_setup;
pub mod agents;
pub mod distro;
pub mod doctor;
pub mod git;
pub mod git_changes;
+7 -1
View File
@@ -2,7 +2,9 @@ mod commands;
mod services;
mod types;
use services::distro_bundle::DistroBundleState;
use services::personas::PersonaStore;
use tauri::Manager;
use tauri_plugin_window_state::StateFlags;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -30,6 +32,10 @@ pub fn run() {
let builder = builder.plugin(tauri_plugin_app_test_driver::init());
builder
.setup(|app| {
app.manage(DistroBundleState::new(app.handle()));
Ok(())
})
.invoke_handler(tauri::generate_handler![
commands::agents::list_personas,
commands::agents::create_persona,
@@ -72,6 +78,7 @@ pub fn run() {
commands::agent_setup::install_agent,
commands::agent_setup::authenticate_agent,
commands::path_resolver::resolve_path,
commands::distro::get_distro_bundle,
commands::system::get_home_dir,
commands::system::save_exported_session_file,
commands::system::path_exists,
@@ -80,7 +87,6 @@ pub fn run() {
commands::system::list_files_for_mentions,
commands::system::read_image_attachment,
])
.setup(|_app| Ok(()))
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|_app, _event| {});
@@ -1,8 +1,12 @@
use tauri::Manager;
use tauri_plugin_shell::ShellExt;
use std::ffi::OsString;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use crate::services::distro_bundle::DistroBundleState;
use tokio::process::{Child, Command};
use tokio::sync::OnceCell;
@@ -68,6 +72,18 @@ impl GooseServeProcess {
let mut command: Command = get_goose_command(&app_handle)?;
let binary_display = command.as_std().get_program().to_string_lossy().to_string();
if let Some(distro_state) = app_handle.try_state::<DistroBundleState>() {
if let Some(bundle) = distro_state.bundle() {
if let Some(bin_dir) = &bundle.bin_dir {
prepend_path_env(&mut command, bin_dir);
}
if let Some(config_path) = &bundle.config_path {
append_additional_config_env(&mut command, config_path);
}
command.env("GOOSE_DISTRO_DIR", &bundle.root_dir);
}
}
command
.arg("serve")
.arg("--host")
@@ -149,6 +165,54 @@ fn default_serve_working_dir() -> PathBuf {
dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp"))
}
fn prepend_path_env(command: &mut Command, extra_dir: &std::path::Path) {
let mut paths = vec![extra_dir.to_path_buf()];
if let Some(existing) = std::env::var_os("PATH") {
paths.extend(std::env::split_paths(&existing));
}
set_path_list_env(command, "PATH", paths, Some(extra_dir.as_os_str()));
}
fn append_additional_config_env(command: &mut Command, config_path: &std::path::Path) {
let existing = std::env::var_os("GOOSE_ADDITIONAL_CONFIG_FILES");
let mut paths: Vec<PathBuf> = existing
.as_ref()
.map(std::env::split_paths)
.map(Iterator::collect)
.unwrap_or_default();
paths.push(config_path.to_path_buf());
if let Ok(joined) = std::env::join_paths(&paths) {
command.env("GOOSE_ADDITIONAL_CONFIG_FILES", joined);
} else {
let mut fallback = existing.unwrap_or_default();
if !fallback.is_empty() {
fallback.push(if cfg!(windows) { ";" } else { ":" });
}
fallback.push(config_path.as_os_str());
command.env("GOOSE_ADDITIONAL_CONFIG_FILES", fallback);
}
}
fn set_path_list_env(
command: &mut Command,
key: &str,
paths: Vec<PathBuf>,
fallback_prefix: Option<&std::ffi::OsStr>,
) {
if let Ok(joined) = std::env::join_paths(&paths) {
command.env(key, joined);
} else if let Some(prefix) = fallback_prefix {
let mut fallback = OsString::from(prefix);
for path in paths.iter().skip(1) {
fallback.push(if cfg!(windows) { ";" } else { ":" });
fallback.push(path.as_os_str());
}
command.env(key, fallback);
}
}
fn reserve_free_port() -> Result<u16, String> {
let listener = std::net::TcpListener::bind((LOCALHOST, 0))
.map_err(|error| format!("Failed to reserve Goose serve port: {error}"))?;
@@ -0,0 +1,164 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::path::{Path, PathBuf};
use tauri::{AppHandle, Manager};
const DISTRO_DIR_NAME: &str = "distro";
const DISTRO_JSON_NAME: &str = "distro.json";
const DISTRO_CONFIG_NAME: &str = "config.yaml";
const DISTRO_BIN_DIR_NAME: &str = "bin";
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DistroManifest {
pub app_version: Option<String>,
pub feature_toggles: Option<HashMap<String, bool>>,
pub extension_allowlist: Option<String>,
pub provider_allowlist: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DistroBundleInfo {
pub present: bool,
pub app_version: Option<String>,
pub feature_toggles: Option<HashMap<String, bool>>,
pub extension_allowlist: Option<String>,
pub provider_allowlist: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DistroBundle {
pub root_dir: PathBuf,
pub config_path: Option<PathBuf>,
pub bin_dir: Option<PathBuf>,
pub manifest: DistroManifest,
}
pub struct DistroBundleState {
bundle: Option<DistroBundle>,
}
impl DistroBundleState {
pub fn new(app_handle: &AppHandle) -> Self {
let bundle = load_distro_bundle(app_handle)
.map_err(|error| {
log::warn!("Failed to load distro bundle: {error}");
error
})
.ok()
.flatten();
Self { bundle }
}
pub fn info(&self) -> DistroBundleInfo {
let Some(bundle) = &self.bundle else {
return DistroBundleInfo {
present: false,
app_version: None,
feature_toggles: None,
extension_allowlist: None,
provider_allowlist: None,
};
};
DistroBundleInfo {
present: true,
app_version: bundle.manifest.app_version.clone(),
feature_toggles: bundle.manifest.feature_toggles.clone(),
extension_allowlist: bundle.manifest.extension_allowlist.clone(),
provider_allowlist: bundle.manifest.provider_allowlist.clone(),
}
}
pub fn bundle(&self) -> Option<&DistroBundle> {
self.bundle.as_ref()
}
}
fn load_distro_bundle(app_handle: &AppHandle) -> Result<Option<DistroBundle>, String> {
let Some(root_dir) = resolve_distro_root(app_handle)? else {
return Ok(None);
};
let manifest_path = root_dir.join(DISTRO_JSON_NAME);
if !manifest_path.exists() {
return Ok(None);
}
let manifest = read_manifest(&manifest_path)?;
let config_path = root_dir.join(DISTRO_CONFIG_NAME);
let bin_dir = root_dir.join(DISTRO_BIN_DIR_NAME);
Ok(Some(DistroBundle {
root_dir,
config_path: config_path.exists().then_some(config_path),
bin_dir: bin_dir.is_dir().then_some(bin_dir),
manifest,
}))
}
fn resolve_distro_root(app_handle: &AppHandle) -> Result<Option<PathBuf>, String> {
if let Ok(override_dir) = env::var("GOOSE_DISTRO_DIR") {
let path = PathBuf::from(override_dir);
if path.is_dir() {
return Ok(Some(path));
}
return Err(format!(
"GOOSE_DISTRO_DIR points to a non-directory path: {}",
path.display()
));
}
let resource_dir = app_handle
.path()
.resource_dir()
.map_err(|error| format!("Failed to resolve Tauri resource dir: {error}"))?;
let distro_dir = resource_dir.join(DISTRO_DIR_NAME);
Ok(distro_dir.is_dir().then_some(distro_dir))
}
fn read_manifest(path: &Path) -> Result<DistroManifest, String> {
let contents = std::fs::read_to_string(path).map_err(|error| {
format!(
"Failed to read distro manifest '{}': {error}",
path.display()
)
})?;
serde_json::from_str::<DistroManifest>(&contents).map_err(|error| {
format!(
"Failed to parse distro manifest '{}': {error}",
path.display()
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_partial_manifest() {
let manifest = serde_json::from_str::<DistroManifest>(
r#"{
"appVersion": "development",
"featureToggles": {"foo": true}
}"#,
)
.expect("manifest should parse");
assert_eq!(manifest.app_version.as_deref(), Some("development"));
assert_eq!(
manifest
.feature_toggles
.as_ref()
.and_then(|toggles| toggles.get("foo"))
.copied(),
Some(true)
);
}
}
+1
View File
@@ -1,2 +1,3 @@
pub mod acp;
pub mod distro_bundle;
pub mod personas;
+3
View File
@@ -46,6 +46,9 @@
"icons/icon.icns",
"icons/icon.ico"
],
"resources": {
"../distro": "distro"
},
"externalBin": ["../../../target/release/goose"],
"macOS": {
"entitlements": "entitlements.plist",
+33 -1
View File
@@ -6,6 +6,9 @@ import { discoverAcpProvidersFromEntries } from "@/shared/api/acp";
import { setNotificationHandler, getClient } from "@/shared/api/acpConnection";
import notificationHandler from "@/shared/api/acpNotificationHandler";
import { perfLog } from "@/shared/lib/perfLog";
import { parseProviderAllowlist } from "@/features/providers/distroProviderConstraints";
import { getModelProviders } from "@/features/providers/providerCatalog";
import { useDistroStore } from "@/features/settings/stores/distroStore";
export function useAppStartup() {
useEffect(() => {
@@ -25,6 +28,18 @@ export function useAppStartup() {
const store = useAgentStore.getState();
const inventoryStore = useProviderInventoryStore.getState();
const distroStore = useDistroStore.getState();
const loadDistroBundle = async () => {
try {
const { getDistroBundle } = await import("@/shared/api/distro");
const manifest = await getDistroBundle();
distroStore.setManifest(manifest);
} catch (err) {
console.error("Failed to load distro bundle on startup:", err);
distroStore.setManifest({ present: false });
}
};
const loadPersonas = async () => {
const t0 = performance.now();
store.setPersonasLoading(true);
@@ -57,7 +72,22 @@ export function useAppStartup() {
// Derive ACP providers from the same response
const providers = discoverAcpProvidersFromEntries(entries);
store.setProviders(providers);
const providerAllowlist = parseProviderAllowlist(
useDistroStore.getState().manifest,
);
if (!providerAllowlist) {
store.setProviders(providers);
} else {
const hasAllowedModelProvider = getModelProviders().some(
(provider) => providerAllowlist.has(provider.id),
);
store.setProviders(
providers.filter(
(provider) =>
provider.id !== "goose" || hasAllowedModelProvider,
),
);
}
perfLog(
`[perf:startup] loadProvidersAndInventory done in ${(performance.now() - t0).toFixed(1)}ms (entries=${entries.length}, providers=${providers.length})`,
@@ -87,6 +117,8 @@ export function useAppStartup() {
setActiveSession(null);
};
await loadDistroBundle();
const providersAndInventoryLoad = loadProvidersAndInventory();
await Promise.allSettled([
@@ -0,0 +1,63 @@
import { describe, expect, it } from "vitest";
import { filterModelProvidersForDistro } from "./distroProviderConstraints";
describe("filterModelProvidersForDistro", () => {
const providers = [
{
id: "anthropic",
displayName: "Anthropic",
category: "model",
description: "Claude models",
setupMethod: "single_api_key",
tier: "promoted",
},
{
id: "openai",
displayName: "OpenAI",
category: "model",
description: "GPT models",
setupMethod: "single_api_key",
tier: "promoted",
},
{
id: "ollama",
displayName: "Ollama",
category: "model",
description: "Local models",
setupMethod: "local",
tier: "promoted",
},
] as const;
it("returns all providers when no distro is present", () => {
expect(
filterModelProvidersForDistro([...providers], { present: false }),
).toEqual(providers);
});
it("returns all providers when no allowlist is configured", () => {
expect(
filterModelProvidersForDistro([...providers], {
present: true,
}),
).toEqual(providers);
});
it("filters providers to the configured allowlist", () => {
expect(
filterModelProvidersForDistro([...providers], {
present: true,
providerAllowlist: "openai, ollama",
}),
).toEqual([providers[1], providers[2]]);
});
it("ignores whitespace and empty allowlist items", () => {
expect(
filterModelProvidersForDistro([...providers], {
present: true,
providerAllowlist: " anthropic ,, openai ",
}),
).toEqual([providers[0], providers[1]]);
});
});
@@ -0,0 +1,34 @@
import type { ProviderCatalogEntry } from "@/shared/types/providers";
import type { DistroBundleInfo } from "@/shared/types/distro";
export function parseProviderAllowlist(
distro: DistroBundleInfo | null | undefined,
): Set<string> | null {
if (!distro?.present) {
return null;
}
const raw = distro.providerAllowlist?.trim();
if (!raw) {
return null;
}
const providerIds = raw
.split(",")
.map((providerId) => providerId.trim())
.filter(Boolean);
return providerIds.length > 0 ? new Set(providerIds) : null;
}
export function filterModelProvidersForDistro(
providers: ProviderCatalogEntry[],
distro: DistroBundleInfo | null | undefined,
): ProviderCatalogEntry[] {
const allowlist = parseProviderAllowlist(distro);
if (!allowlist) {
return providers;
}
return providers.filter((provider) => allowlist.has(provider.id));
}
@@ -6,27 +6,22 @@ import type {
ProviderInventoryModelDto,
} from "@aaif/goose-sdk";
import { getModelProviders } from "../providerCatalog";
const MODEL_PROVIDER_IDS = new Set(getModelProviders().map((p) => p.id));
import { useDistroStore } from "@/features/settings/stores/distroStore";
import { filterModelProvidersForDistro } from "../distroProviderConstraints";
function isConfiguredGooseModelProvider(
entry: ProviderInventoryEntryDto,
modelProviderIds: Set<string>,
): boolean {
if (!entry.configured) {
return false;
}
const isCuratedModelProvider = MODEL_PROVIDER_IDS.has(entry.providerId);
if (entry.providerType === "Custom") {
return entry.providerId.startsWith("custom_");
}
if (entry.providerType === "Declarative") {
return isCuratedModelProvider;
}
return isCuratedModelProvider;
return modelProviderIds.has(entry.providerId);
}
function inventoryModelToOption(
@@ -48,6 +43,7 @@ function inventoryModelToOption(
export function useProviderInventory() {
const entries = useProviderInventoryStore((s) => s.entries);
const loading = useProviderInventoryStore((s) => s.loading);
const distro = useDistroStore((s) => s.manifest);
const getEntry = useCallback(
(providerId: string) => entries.get(providerId),
@@ -63,9 +59,22 @@ export function useProviderInventory() {
[entries],
);
const modelProviderIds = useMemo(
() =>
new Set(
filterModelProvidersForDistro(getModelProviders(), distro).map(
(provider) => provider.id,
),
),
[distro],
);
const configuredModelProviderEntries = useMemo(
() => [...entries.values()].filter(isConfiguredGooseModelProvider),
[entries],
() =>
[...entries.values()].filter((entry) =>
isConfiguredGooseModelProvider(entry, modelProviderIds),
),
[entries, modelProviderIds],
);
const getModelsForAgent = useCallback(
@@ -84,8 +93,8 @@ export function useProviderInventory() {
const configuredProviderIds = useMemo(
() =>
[...entries.values()]
.filter((e) => e.configured)
.map((e) => e.providerId),
.filter((entry) => entry.configured)
.map((entry) => entry.providerId),
[entries],
);
@@ -0,0 +1,18 @@
import { create } from "zustand";
import type { DistroBundleInfo } from "@/shared/types/distro";
interface DistroState {
loaded: boolean;
manifest: DistroBundleInfo;
setManifest: (manifest: DistroBundleInfo) => void;
}
const EMPTY_DISTRO: DistroBundleInfo = {
present: false,
};
export const useDistroStore = create<DistroState>((set) => ({
loaded: false,
manifest: EMPTY_DISTRO,
setManifest: (manifest) => set({ manifest, loaded: true }),
}));
@@ -19,6 +19,8 @@ import {
getModelProviders,
} from "@/features/providers/providerCatalog";
import { useCredentials } from "@/features/providers/hooks/useCredentials";
import { useDistroStore } from "@/features/settings/stores/distroStore";
import { filterModelProvidersForDistro } from "@/features/providers/distroProviderConstraints";
import { useCustomProviders } from "@/features/providers/hooks/useCustomProviders";
import {
CustomProviderChoice,
@@ -100,6 +102,7 @@ interface PendingCustomProviderDelete {
export function ProvidersSettings() {
const { t } = useTranslation(["settings", "common"]);
const distro = useDistroStore((state) => state.manifest);
const [showAllModels, setShowAllModels] = useState(false);
const [modelOrder, setModelOrder] = useState<string[] | null>(null);
const [customDialogOpen, setCustomDialogOpen] = useState(false);
@@ -137,8 +140,12 @@ export function ProvidersSettings() {
);
const allModels = useMemo(
() => toDisplayInfo(getModelProviders(), configuredIds),
[configuredIds],
() =>
toDisplayInfo(
filterModelProvidersForDistro(getModelProviders(), distro),
configuredIds,
),
[configuredIds, distro],
);
const sortedModels = useMemo(() => {
+6
View File
@@ -0,0 +1,6 @@
import { invoke } from "@tauri-apps/api/core";
import type { DistroBundleInfo } from "@/shared/types/distro";
export async function getDistroBundle(): Promise<DistroBundleInfo> {
return invoke("get_distro_bundle");
}
+1
View File
@@ -1,4 +1,5 @@
export * from "./agents";
export * from "./acp";
export * from "./distro";
export * from "./git";
export * from "./pathResolver";
+7
View File
@@ -0,0 +1,7 @@
export interface DistroBundleInfo {
present: boolean;
appVersion?: string;
featureToggles?: Record<string, boolean>;
extensionAllowlist?: string;
providerAllowlist?: string;
}
+1
View File
@@ -1,3 +1,4 @@
export * from "./distro";
export * from "./messages";
export * from "./agents";
export * from "./chat";
@@ -12,6 +12,7 @@ import type { ComponentProps } from "react";
import { createContext, useContext, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { getUsage } from "tokenlens";
import { useDistroStore } from "@/features/settings/stores/distroStore";
const PERCENT_MAX = 100;
const ICON_RADIUS = 10;
@@ -61,6 +62,13 @@ export const Context = ({
);
};
function useCostTrackingEnabled() {
const featureToggles = useDistroStore(
(state) => state.manifest.featureToggles,
);
return featureToggles?.costTracking !== false;
}
const ContextIcon = () => {
const { t } = useTranslation("common");
const { usedTokens, maxTokens } = useContextValue();
@@ -199,6 +207,7 @@ export const ContextContentFooter = ({
className,
...props
}: ContextContentFooterProps) => {
const costTrackingEnabled = useCostTrackingEnabled();
const { t } = useTranslation("common");
const { formatNumber } = useLocaleFormatting();
const { modelId, usage } = useContextValue();
@@ -216,6 +225,10 @@ export const ContextContentFooter = ({
style: "currency",
});
if (!costTrackingEnabled) {
return null;
}
return (
<div
className={cn(
@@ -241,6 +254,7 @@ const TokensWithCost = ({
tokens?: number;
costText?: string;
}) => {
const costTrackingEnabled = useCostTrackingEnabled();
const { formatNumber } = useLocaleFormatting();
return (
@@ -250,7 +264,7 @@ const TokensWithCost = ({
: formatNumber(tokens, {
notation: "compact",
})}
{costText ? (
{costTrackingEnabled && costText ? (
<span className="ml-2 text-muted-foreground"> {costText}</span>
) : null}
</span>
@@ -34,6 +34,9 @@ export function buildInitScript(options?: {
const PERSONAS = ${personas};
const SKILLS = ${skills};
const PROJECTS = ${projects};
const DISTRO = {
present: false,
};
const FAKE_ACP_URL = "ws://127.0.0.1:0/mock-acp";
const ACP_SESSIONS = [];
const PROVIDER_INVENTORY = [
@@ -285,6 +288,8 @@ export function buildInitScript(options?: {
// ---- ACP transport ----
case "get_goose_serve_url":
return Promise.resolve(FAKE_ACP_URL);
case "get_distro_bundle":
return Promise.resolve(DISTRO);
// ---- Personas ----
case "list_personas":