goose2 distribution bundling (#8911)
This commit is contained in:
@@ -151,14 +151,10 @@ fn system_config_path() -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bundled_defaults_path() -> Option<PathBuf> {
|
fn additional_config_paths_from_env() -> Vec<PathBuf> {
|
||||||
let exe = std::env::current_exe().ok()?;
|
env::var_os("GOOSE_ADDITIONAL_CONFIG_FILES")
|
||||||
let path = exe.parent()?.join("defaults.yaml");
|
.map(|value| env::split_paths(&value).collect())
|
||||||
if path.exists() {
|
.unwrap_or_default()
|
||||||
Some(path)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@@ -167,9 +163,7 @@ impl Default for Config {
|
|||||||
let user_config_path = config_dir.join(CONFIG_YAML_NAME);
|
let user_config_path = config_dir.join(CONFIG_YAML_NAME);
|
||||||
|
|
||||||
let mut config_paths = vec![system_config_path()];
|
let mut config_paths = vec![system_config_path()];
|
||||||
if let Some(defaults) = bundled_defaults_path() {
|
config_paths.extend(additional_config_paths_from_env());
|
||||||
config_paths.insert(0, defaults);
|
|
||||||
}
|
|
||||||
config_paths.push(user_config_path.clone());
|
config_paths.push(user_config_path.clone());
|
||||||
|
|
||||||
let no_secrets_config = Self {
|
let no_secrets_config = Self {
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ ThemeProvider manages three axes:
|
|||||||
- Title bar uses `titleBarStyle: "Overlay"` with `hiddenTitle: true` for a custom titlebar.
|
- Title bar uses `titleBarStyle: "Overlay"` with `hiddenTitle: true` for a custom titlebar.
|
||||||
- `tauri-plugin-window-state` persists window size and position.
|
- `tauri-plugin-window-state` persists window size and position.
|
||||||
- Traffic light offset: `pl-20` (80px) to accommodate macOS window controls.
|
- Traffic light offset: `pl-20` (80px) to accommodate macOS window controls.
|
||||||
|
- Distro bundle behavior, including feature flags, is documented in `distro/README.md`.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"featureToggles": {
|
||||||
|
"costTracking": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -90,6 +90,10 @@ dev: setup
|
|||||||
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
|
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
|
||||||
PROJECT_DIR=$(pwd)
|
PROJECT_DIR=$(pwd)
|
||||||
REPO_ROOT=$(cd ../.. && 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_DEBUG="${REPO_ROOT}/target/debug/goose"
|
||||||
LOCAL_GOOSE_RELEASE="${REPO_ROOT}/target/release/goose"
|
LOCAL_GOOSE_RELEASE="${REPO_ROOT}/target/release/goose"
|
||||||
if [[ -x "${LOCAL_GOOSE_DEBUG}" ]]; then
|
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"
|
echo "No local goose binary found under ${REPO_ROOT}/target; falling back to PATH"
|
||||||
fi
|
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
|
# In worktrees, generate a labeled icon so you can tell instances apart
|
||||||
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||||
GIT_DIR=$(git rev-parse --git-dir)
|
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.
|
# Override with e.g. RUST_LOG=info just dev-debug to disable.
|
||||||
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
|
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
|
||||||
REPO_ROOT=$(cd ../.. && pwd)
|
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_DEBUG="${REPO_ROOT}/target/debug/goose"
|
||||||
LOCAL_GOOSE_RELEASE="${REPO_ROOT}/target/release/goose"
|
LOCAL_GOOSE_RELEASE="${REPO_ROOT}/target/release/goose"
|
||||||
if [[ -x "${LOCAL_GOOSE_DEBUG}" ]]; then
|
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"
|
echo "No local goose binary found under ${REPO_ROOT}/target; falling back to PATH"
|
||||||
fi
|
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
|
# In worktrees, generate a labeled icon so you can tell instances apart
|
||||||
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||||
GIT_DIR=$(git rev-parse --git-dir)
|
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,6 +1,7 @@
|
|||||||
pub mod acp;
|
pub mod acp;
|
||||||
pub mod agent_setup;
|
pub mod agent_setup;
|
||||||
pub mod agents;
|
pub mod agents;
|
||||||
|
pub mod distro;
|
||||||
pub mod doctor;
|
pub mod doctor;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod git_changes;
|
pub mod git_changes;
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ mod commands;
|
|||||||
mod services;
|
mod services;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
|
use services::distro_bundle::DistroBundleState;
|
||||||
use services::personas::PersonaStore;
|
use services::personas::PersonaStore;
|
||||||
|
use tauri::Manager;
|
||||||
use tauri_plugin_window_state::StateFlags;
|
use tauri_plugin_window_state::StateFlags;
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@@ -30,6 +32,10 @@ pub fn run() {
|
|||||||
let builder = builder.plugin(tauri_plugin_app_test_driver::init());
|
let builder = builder.plugin(tauri_plugin_app_test_driver::init());
|
||||||
|
|
||||||
builder
|
builder
|
||||||
|
.setup(|app| {
|
||||||
|
app.manage(DistroBundleState::new(app.handle()));
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
commands::agents::list_personas,
|
commands::agents::list_personas,
|
||||||
commands::agents::create_persona,
|
commands::agents::create_persona,
|
||||||
@@ -72,6 +78,7 @@ pub fn run() {
|
|||||||
commands::agent_setup::install_agent,
|
commands::agent_setup::install_agent,
|
||||||
commands::agent_setup::authenticate_agent,
|
commands::agent_setup::authenticate_agent,
|
||||||
commands::path_resolver::resolve_path,
|
commands::path_resolver::resolve_path,
|
||||||
|
commands::distro::get_distro_bundle,
|
||||||
commands::system::get_home_dir,
|
commands::system::get_home_dir,
|
||||||
commands::system::save_exported_session_file,
|
commands::system::save_exported_session_file,
|
||||||
commands::system::path_exists,
|
commands::system::path_exists,
|
||||||
@@ -80,7 +87,6 @@ pub fn run() {
|
|||||||
commands::system::list_files_for_mentions,
|
commands::system::list_files_for_mentions,
|
||||||
commands::system::read_image_attachment,
|
commands::system::read_image_attachment,
|
||||||
])
|
])
|
||||||
.setup(|_app| Ok(()))
|
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while building tauri application")
|
.expect("error while building tauri application")
|
||||||
.run(|_app, _event| {});
|
.run(|_app, _event| {});
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
use tauri::Manager;
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
|
use std::ffi::OsString;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crate::services::distro_bundle::DistroBundleState;
|
||||||
|
|
||||||
use tokio::process::{Child, Command};
|
use tokio::process::{Child, Command};
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
@@ -68,6 +72,18 @@ impl GooseServeProcess {
|
|||||||
let mut command: Command = get_goose_command(&app_handle)?;
|
let mut command: Command = get_goose_command(&app_handle)?;
|
||||||
let binary_display = command.as_std().get_program().to_string_lossy().to_string();
|
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
|
command
|
||||||
.arg("serve")
|
.arg("serve")
|
||||||
.arg("--host")
|
.arg("--host")
|
||||||
@@ -149,6 +165,54 @@ fn default_serve_working_dir() -> PathBuf {
|
|||||||
dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp"))
|
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> {
|
fn reserve_free_port() -> Result<u16, String> {
|
||||||
let listener = std::net::TcpListener::bind((LOCALHOST, 0))
|
let listener = std::net::TcpListener::bind((LOCALHOST, 0))
|
||||||
.map_err(|error| format!("Failed to reserve Goose serve port: {error}"))?;
|
.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,2 +1,3 @@
|
|||||||
pub mod acp;
|
pub mod acp;
|
||||||
|
pub mod distro_bundle;
|
||||||
pub mod personas;
|
pub mod personas;
|
||||||
|
|||||||
@@ -46,6 +46,9 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
|
"resources": {
|
||||||
|
"../distro": "distro"
|
||||||
|
},
|
||||||
"externalBin": ["../../../target/release/goose"],
|
"externalBin": ["../../../target/release/goose"],
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"entitlements": "entitlements.plist",
|
"entitlements": "entitlements.plist",
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { discoverAcpProvidersFromEntries } from "@/shared/api/acp";
|
|||||||
import { setNotificationHandler, getClient } from "@/shared/api/acpConnection";
|
import { setNotificationHandler, getClient } from "@/shared/api/acpConnection";
|
||||||
import notificationHandler from "@/shared/api/acpNotificationHandler";
|
import notificationHandler from "@/shared/api/acpNotificationHandler";
|
||||||
import { perfLog } from "@/shared/lib/perfLog";
|
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() {
|
export function useAppStartup() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -25,6 +28,18 @@ export function useAppStartup() {
|
|||||||
|
|
||||||
const store = useAgentStore.getState();
|
const store = useAgentStore.getState();
|
||||||
const inventoryStore = useProviderInventoryStore.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 loadPersonas = async () => {
|
||||||
const t0 = performance.now();
|
const t0 = performance.now();
|
||||||
store.setPersonasLoading(true);
|
store.setPersonasLoading(true);
|
||||||
@@ -57,7 +72,22 @@ export function useAppStartup() {
|
|||||||
|
|
||||||
// Derive ACP providers from the same response
|
// Derive ACP providers from the same response
|
||||||
const providers = discoverAcpProvidersFromEntries(entries);
|
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(
|
perfLog(
|
||||||
`[perf:startup] loadProvidersAndInventory done in ${(performance.now() - t0).toFixed(1)}ms (entries=${entries.length}, providers=${providers.length})`,
|
`[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);
|
setActiveSession(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await loadDistroBundle();
|
||||||
|
|
||||||
const providersAndInventoryLoad = loadProvidersAndInventory();
|
const providersAndInventoryLoad = loadProvidersAndInventory();
|
||||||
|
|
||||||
await Promise.allSettled([
|
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,
|
ProviderInventoryModelDto,
|
||||||
} from "@aaif/goose-sdk";
|
} from "@aaif/goose-sdk";
|
||||||
import { getModelProviders } from "../providerCatalog";
|
import { getModelProviders } from "../providerCatalog";
|
||||||
|
import { useDistroStore } from "@/features/settings/stores/distroStore";
|
||||||
const MODEL_PROVIDER_IDS = new Set(getModelProviders().map((p) => p.id));
|
import { filterModelProvidersForDistro } from "../distroProviderConstraints";
|
||||||
|
|
||||||
function isConfiguredGooseModelProvider(
|
function isConfiguredGooseModelProvider(
|
||||||
entry: ProviderInventoryEntryDto,
|
entry: ProviderInventoryEntryDto,
|
||||||
|
modelProviderIds: Set<string>,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!entry.configured) {
|
if (!entry.configured) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCuratedModelProvider = MODEL_PROVIDER_IDS.has(entry.providerId);
|
|
||||||
|
|
||||||
if (entry.providerType === "Custom") {
|
if (entry.providerType === "Custom") {
|
||||||
return entry.providerId.startsWith("custom_");
|
return entry.providerId.startsWith("custom_");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.providerType === "Declarative") {
|
return modelProviderIds.has(entry.providerId);
|
||||||
return isCuratedModelProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isCuratedModelProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function inventoryModelToOption(
|
function inventoryModelToOption(
|
||||||
@@ -48,6 +43,7 @@ function inventoryModelToOption(
|
|||||||
export function useProviderInventory() {
|
export function useProviderInventory() {
|
||||||
const entries = useProviderInventoryStore((s) => s.entries);
|
const entries = useProviderInventoryStore((s) => s.entries);
|
||||||
const loading = useProviderInventoryStore((s) => s.loading);
|
const loading = useProviderInventoryStore((s) => s.loading);
|
||||||
|
const distro = useDistroStore((s) => s.manifest);
|
||||||
|
|
||||||
const getEntry = useCallback(
|
const getEntry = useCallback(
|
||||||
(providerId: string) => entries.get(providerId),
|
(providerId: string) => entries.get(providerId),
|
||||||
@@ -63,9 +59,22 @@ export function useProviderInventory() {
|
|||||||
[entries],
|
[entries],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const modelProviderIds = useMemo(
|
||||||
|
() =>
|
||||||
|
new Set(
|
||||||
|
filterModelProvidersForDistro(getModelProviders(), distro).map(
|
||||||
|
(provider) => provider.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[distro],
|
||||||
|
);
|
||||||
|
|
||||||
const configuredModelProviderEntries = useMemo(
|
const configuredModelProviderEntries = useMemo(
|
||||||
() => [...entries.values()].filter(isConfiguredGooseModelProvider),
|
() =>
|
||||||
[entries],
|
[...entries.values()].filter((entry) =>
|
||||||
|
isConfiguredGooseModelProvider(entry, modelProviderIds),
|
||||||
|
),
|
||||||
|
[entries, modelProviderIds],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getModelsForAgent = useCallback(
|
const getModelsForAgent = useCallback(
|
||||||
@@ -84,8 +93,8 @@ export function useProviderInventory() {
|
|||||||
const configuredProviderIds = useMemo(
|
const configuredProviderIds = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[...entries.values()]
|
[...entries.values()]
|
||||||
.filter((e) => e.configured)
|
.filter((entry) => entry.configured)
|
||||||
.map((e) => e.providerId),
|
.map((entry) => entry.providerId),
|
||||||
[entries],
|
[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,
|
getModelProviders,
|
||||||
} from "@/features/providers/providerCatalog";
|
} from "@/features/providers/providerCatalog";
|
||||||
import { useCredentials } from "@/features/providers/hooks/useCredentials";
|
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 { useCustomProviders } from "@/features/providers/hooks/useCustomProviders";
|
||||||
import {
|
import {
|
||||||
CustomProviderChoice,
|
CustomProviderChoice,
|
||||||
@@ -100,6 +102,7 @@ interface PendingCustomProviderDelete {
|
|||||||
|
|
||||||
export function ProvidersSettings() {
|
export function ProvidersSettings() {
|
||||||
const { t } = useTranslation(["settings", "common"]);
|
const { t } = useTranslation(["settings", "common"]);
|
||||||
|
const distro = useDistroStore((state) => state.manifest);
|
||||||
const [showAllModels, setShowAllModels] = useState(false);
|
const [showAllModels, setShowAllModels] = useState(false);
|
||||||
const [modelOrder, setModelOrder] = useState<string[] | null>(null);
|
const [modelOrder, setModelOrder] = useState<string[] | null>(null);
|
||||||
const [customDialogOpen, setCustomDialogOpen] = useState(false);
|
const [customDialogOpen, setCustomDialogOpen] = useState(false);
|
||||||
@@ -137,8 +140,12 @@ export function ProvidersSettings() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const allModels = useMemo(
|
const allModels = useMemo(
|
||||||
() => toDisplayInfo(getModelProviders(), configuredIds),
|
() =>
|
||||||
[configuredIds],
|
toDisplayInfo(
|
||||||
|
filterModelProvidersForDistro(getModelProviders(), distro),
|
||||||
|
configuredIds,
|
||||||
|
),
|
||||||
|
[configuredIds, distro],
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortedModels = useMemo(() => {
|
const sortedModels = useMemo(() => {
|
||||||
|
|||||||
@@ -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,4 +1,5 @@
|
|||||||
export * from "./agents";
|
export * from "./agents";
|
||||||
export * from "./acp";
|
export * from "./acp";
|
||||||
|
export * from "./distro";
|
||||||
export * from "./git";
|
export * from "./git";
|
||||||
export * from "./pathResolver";
|
export * from "./pathResolver";
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export interface DistroBundleInfo {
|
||||||
|
present: boolean;
|
||||||
|
appVersion?: string;
|
||||||
|
featureToggles?: Record<string, boolean>;
|
||||||
|
extensionAllowlist?: string;
|
||||||
|
providerAllowlist?: string;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from "./distro";
|
||||||
export * from "./messages";
|
export * from "./messages";
|
||||||
export * from "./agents";
|
export * from "./agents";
|
||||||
export * from "./chat";
|
export * from "./chat";
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { ComponentProps } from "react";
|
|||||||
import { createContext, useContext, useMemo } from "react";
|
import { createContext, useContext, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getUsage } from "tokenlens";
|
import { getUsage } from "tokenlens";
|
||||||
|
import { useDistroStore } from "@/features/settings/stores/distroStore";
|
||||||
|
|
||||||
const PERCENT_MAX = 100;
|
const PERCENT_MAX = 100;
|
||||||
const ICON_RADIUS = 10;
|
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 ContextIcon = () => {
|
||||||
const { t } = useTranslation("common");
|
const { t } = useTranslation("common");
|
||||||
const { usedTokens, maxTokens } = useContextValue();
|
const { usedTokens, maxTokens } = useContextValue();
|
||||||
@@ -199,6 +207,7 @@ export const ContextContentFooter = ({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ContextContentFooterProps) => {
|
}: ContextContentFooterProps) => {
|
||||||
|
const costTrackingEnabled = useCostTrackingEnabled();
|
||||||
const { t } = useTranslation("common");
|
const { t } = useTranslation("common");
|
||||||
const { formatNumber } = useLocaleFormatting();
|
const { formatNumber } = useLocaleFormatting();
|
||||||
const { modelId, usage } = useContextValue();
|
const { modelId, usage } = useContextValue();
|
||||||
@@ -216,6 +225,10 @@ export const ContextContentFooter = ({
|
|||||||
style: "currency",
|
style: "currency",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!costTrackingEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -241,6 +254,7 @@ const TokensWithCost = ({
|
|||||||
tokens?: number;
|
tokens?: number;
|
||||||
costText?: string;
|
costText?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const costTrackingEnabled = useCostTrackingEnabled();
|
||||||
const { formatNumber } = useLocaleFormatting();
|
const { formatNumber } = useLocaleFormatting();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -250,7 +264,7 @@ const TokensWithCost = ({
|
|||||||
: formatNumber(tokens, {
|
: formatNumber(tokens, {
|
||||||
notation: "compact",
|
notation: "compact",
|
||||||
})}
|
})}
|
||||||
{costText ? (
|
{costTrackingEnabled && costText ? (
|
||||||
<span className="ml-2 text-muted-foreground">• {costText}</span>
|
<span className="ml-2 text-muted-foreground">• {costText}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ export function buildInitScript(options?: {
|
|||||||
const PERSONAS = ${personas};
|
const PERSONAS = ${personas};
|
||||||
const SKILLS = ${skills};
|
const SKILLS = ${skills};
|
||||||
const PROJECTS = ${projects};
|
const PROJECTS = ${projects};
|
||||||
|
const DISTRO = {
|
||||||
|
present: false,
|
||||||
|
};
|
||||||
const FAKE_ACP_URL = "ws://127.0.0.1:0/mock-acp";
|
const FAKE_ACP_URL = "ws://127.0.0.1:0/mock-acp";
|
||||||
const ACP_SESSIONS = [];
|
const ACP_SESSIONS = [];
|
||||||
const PROVIDER_INVENTORY = [
|
const PROVIDER_INVENTORY = [
|
||||||
@@ -285,6 +288,8 @@ export function buildInitScript(options?: {
|
|||||||
// ---- ACP transport ----
|
// ---- ACP transport ----
|
||||||
case "get_goose_serve_url":
|
case "get_goose_serve_url":
|
||||||
return Promise.resolve(FAKE_ACP_URL);
|
return Promise.resolve(FAKE_ACP_URL);
|
||||||
|
case "get_distro_bundle":
|
||||||
|
return Promise.resolve(DISTRO);
|
||||||
|
|
||||||
// ---- Personas ----
|
// ---- Personas ----
|
||||||
case "list_personas":
|
case "list_personas":
|
||||||
|
|||||||
Reference in New Issue
Block a user