From 8f5e941f70463d258ac902fad1f64ffca4ec2c3e Mon Sep 17 00:00:00 2001 From: Yidhar <80593214+Yidhar@users.noreply.github.com> Date: Sat, 27 Jun 2026 13:55:29 +0800 Subject: [PATCH 1/2] feat: record the core provider per workspace Workspaces now remember which core provider (session backend) was last used, so reopening a workspace restores it as the default for new sessions instead of always falling back to the global default. - switchyard-session: `Workspace.core_provider: Option`. - gui: `set_workspace_core` command (persists to workspaces.json without bumping updated_at); restore on workspace load/switch; persist when the user picks a core in the sidebar. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/switchyard-gui/frontend/src/App.tsx | 19 ++++++++++++++++- .../frontend/src/services/api.ts | 5 +++++ crates/switchyard-gui/frontend/src/types.ts | 2 ++ crates/switchyard-gui/src/main.rs | 21 +++++++++++++++++++ crates/switchyard-session/src/workspace.rs | 5 +++++ 5 files changed, 51 insertions(+), 1 deletion(-) diff --git a/crates/switchyard-gui/frontend/src/App.tsx b/crates/switchyard-gui/frontend/src/App.tsx index dfcdc32..7b9250a 100644 --- a/crates/switchyard-gui/frontend/src/App.tsx +++ b/crates/switchyard-gui/frontend/src/App.tsx @@ -2377,6 +2377,7 @@ function App() { setWorkspaces(list); const current = await invoke('get_current_workspace'); setCurrentWorkspace(current); + if (current?.core_provider) setNewSessionProvider(current.core_provider); return current; } catch (e) { console.error('Failed to load workspaces:', e); @@ -2390,6 +2391,8 @@ function App() { try { const next = await invoke('set_current_workspace', { workspaceId }); setCurrentWorkspace(next); + // Restore the core provider this workspace last used (or default). + setNewSessionProvider(next.core_provider ?? 'codex'); // Wipe per-workspace UI state — sessions, turns, events all belong // to the previous workspace and would be confusing if left visible. resetWorkspaceScopedUi(); @@ -2402,6 +2405,20 @@ function App() { } }; + // Selecting a core provider persists it on the active workspace so reopening + // the workspace restores the same core. + const handleSelectCoreProvider = (provider: string) => { + setNewSessionProvider(provider); + const ws = currentWorkspace; + if (ws && ws.core_provider !== provider) { + setCurrentWorkspace({ ...ws, core_provider: provider }); + void invoke('set_workspace_core', { + workspaceId: ws.workspace_id, + provider, + }).catch((e) => console.error('failed to persist workspace core', e)); + } + }; + const handleCreateWorkspace = async (primaryRoot: string, name: string | null) => { try { const created = await invoke('create_workspace', { primaryRoot, name }); @@ -5731,7 +5748,7 @@ function App() { selectedSession={selectedSession} config={config} newSessionProvider={newSessionProvider} - setNewSessionProvider={setNewSessionProvider} + setNewSessionProvider={handleSelectCoreProvider} onCreateSession={createNewSession} onSelectSession={selectSession} onDeleteSession={handleDeleteSession} diff --git a/crates/switchyard-gui/frontend/src/services/api.ts b/crates/switchyard-gui/frontend/src/services/api.ts index e5e2fce..767c166 100644 --- a/crates/switchyard-gui/frontend/src/services/api.ts +++ b/crates/switchyard-gui/frontend/src/services/api.ts @@ -38,6 +38,11 @@ export const listKohakuModels = (command?: string): Promise => { return invoke('list_kohaku_models', { command: command ?? null }); }; +// Record the core provider last used in a workspace (restored on reopen). +export const setWorkspaceCore = (workspaceId: string, provider: string): Promise => { + return invoke('set_workspace_core', { workspaceId, provider }); +}; + export const runTurn = ( sessionId: string, message: string, diff --git a/crates/switchyard-gui/frontend/src/types.ts b/crates/switchyard-gui/frontend/src/types.ts index d6cac3f..4acf515 100644 --- a/crates/switchyard-gui/frontend/src/types.ts +++ b/crates/switchyard-gui/frontend/src/types.ts @@ -77,6 +77,8 @@ export interface Workspace { name: string; primary_root: string; extra_roots: string[]; + /// Core provider last used in this workspace (restored on reopen). + core_provider: string | null; created_at: string; updated_at: string; } diff --git a/crates/switchyard-gui/src/main.rs b/crates/switchyard-gui/src/main.rs index c1b9fa8..a0ed20e 100644 --- a/crates/switchyard-gui/src/main.rs +++ b/crates/switchyard-gui/src/main.rs @@ -1614,6 +1614,26 @@ fn update_workspace( Ok(updated) } +/// Record the core provider last used in a workspace so reopening it restores +/// the same default. Persisted in workspaces.json; does not bump `updated_at` +/// (a provider switch shouldn't reorder the recents list). +#[tauri::command] +fn set_workspace_core( + workspace_state: tauri::State<'_, WorkspaceState>, + workspace_id: String, + provider: String, +) -> Result<(), String> { + let id = + uuid::Uuid::parse_str(&workspace_id).map_err(|e| format!("invalid workspace ID: {}", e))?; + workspace_state.mutate(|idx| { + let ws = idx + .get_mut(id) + .ok_or_else(|| format!("workspace {id} not found"))?; + ws.core_provider = Some(provider.clone()).filter(|p| !p.trim().is_empty()); + Ok(()) + }) +} + #[tauri::command] fn delete_workspace( app: tauri::AppHandle, @@ -5096,6 +5116,7 @@ fn main() { clear_current_workspace, create_workspace, update_workspace, + set_workspace_core, delete_workspace, // Filesystem (workspace-scoped) read_file, diff --git a/crates/switchyard-session/src/workspace.rs b/crates/switchyard-session/src/workspace.rs index 28c470e..eca2982 100644 --- a/crates/switchyard-session/src/workspace.rs +++ b/crates/switchyard-session/src/workspace.rs @@ -34,6 +34,10 @@ pub struct Workspace { /// not a CLI argument. #[serde(default)] pub extra_roots: Vec, + /// The core provider (session backend) last used in this workspace, so + /// reopening it restores the same default. `None` until first set. + #[serde(default)] + pub core_provider: Option, pub created_at: DateTime, pub updated_at: DateTime, } @@ -54,6 +58,7 @@ impl Workspace { name, primary_root, extra_roots: Vec::new(), + core_provider: None, created_at: now, updated_at: now, } From 26c6a9a66f26cbb17e32c96d4ae9109962f36199 Mon Sep 17 00:00:00 2001 From: Yidhar <80593214+Yidhar@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:31:10 +0800 Subject: [PATCH 2/2] feat(gui): KohakuTerrarium creature picker + one-click kt-biome install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface kt's ecosystem agents in Settings so kohaku's creature (the kt agent run each turn) is picked from a dropdown instead of hand-typed — the core of integrating kt's config into Switchyard. - gui: `list_kohaku_creatures` parses `kt list` into `@/creatures/` refs (kt-biome / kohaku-creatures); `kohaku_install_biome` runs `kt install @kt-biome`. - Settings (kohaku backend): Creature combobox (datalist of installed creatures/terrariums) bound to args[0], plus an "Install kt-biome" button. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../frontend/src/components/SettingsModal.tsx | 82 ++++++++++++++-- .../frontend/src/services/api.ts | 18 ++++ crates/switchyard-gui/src/main.rs | 96 +++++++++++++++++++ 3 files changed, 190 insertions(+), 6 deletions(-) diff --git a/crates/switchyard-gui/frontend/src/components/SettingsModal.tsx b/crates/switchyard-gui/frontend/src/components/SettingsModal.tsx index bcf8aee..36a5ebd 100644 --- a/crates/switchyard-gui/frontend/src/components/SettingsModal.tsx +++ b/crates/switchyard-gui/frontend/src/components/SettingsModal.tsx @@ -8,7 +8,12 @@ import { defaultProviderConfigFor, inferProviderBackend, } from '../providerCliCapabilities'; -import { listKohakuModels } from '../services/api'; +import { + listKohakuModels, + listKohakuCreatures, + kohakuInstallBiome, + type KohakuCreature, +} from '../services/api'; interface SettingsModalProps { config: SwitchyardConfig; @@ -48,17 +53,37 @@ export const SettingsModal: React.FC = ({ // KohakuTerrarium model selectors are listed dynamically from `kt model // list` (the user's configured llm profiles), like KT's own app. const [kohakuModels, setKohakuModels] = React.useState([]); + const [kohakuCreatures, setKohakuCreatures] = React.useState([]); + const [installingBiome, setInstallingBiome] = React.useState(false); + const [biomeMsg, setBiomeMsg] = React.useState(null); React.useEffect(() => { const prov = config.providers[settingsTab] || defaultProviderConfigFor(settingsTab); if (inferProviderBackend(settingsTab, prov.backend) === 'kohaku') { - listKohakuModels(prov.command || 'kt') - .then(setKohakuModels) - .catch(() => setKohakuModels([])); + const cmd = prov.command || 'kt'; + listKohakuModels(cmd).then(setKohakuModels).catch(() => setKohakuModels([])); + listKohakuCreatures(cmd).then(setKohakuCreatures).catch(() => setKohakuCreatures([])); } else { setKohakuModels([]); + setKohakuCreatures([]); } + setBiomeMsg(null); }, [settingsTab, config]); + const handleInstallBiome = async (command: string) => { + setInstallingBiome(true); + setBiomeMsg(null); + try { + await kohakuInstallBiome(command); + const list = await listKohakuCreatures(command); + setKohakuCreatures(list); + setBiomeMsg(`Installed. ${list.length} creatures/terrariums available.`); + } catch (e) { + setBiomeMsg(`Install failed: ${String(e)}`); + } finally { + setInstallingBiome(false); + } + }; + return (
@@ -186,10 +211,55 @@ export const SettingsModal: React.FC = ({ />
+ {cliMapping.backend === 'kohaku' && ( +
+ + { + const ref = e.target.value.trim(); + const rest = prov.args.slice(1); + onProviderFieldChange(pName, 'args', ref ? [ref, ...rest] : rest); + }} + /> + {kohakuCreatures.length > 0 && ( + + {kohakuCreatures.map((c) => ( + + ))} + + )} +
+ {biomeMsg ?? + (kohakuCreatures.length === 0 + ? 'No creatures found — install the kt-biome pack, or type a config-folder path.' + : 'The kt agent run each turn. Becomes the first CLI argument.')} +
+
+ )} +
- { diff --git a/crates/switchyard-gui/frontend/src/services/api.ts b/crates/switchyard-gui/frontend/src/services/api.ts index 767c166..531f8ac 100644 --- a/crates/switchyard-gui/frontend/src/services/api.ts +++ b/crates/switchyard-gui/frontend/src/services/api.ts @@ -43,6 +43,24 @@ export const setWorkspaceCore = (workspaceId: string, provider: string): Promise return invoke('set_workspace_core', { workspaceId, provider }); }; +// A KohakuTerrarium ecosystem agent (creature/terrarium) from installed packages. +export interface KohakuCreature { + reference: string; + package: string; + name: string; + kind: string; +} + +// List creatures/terrariums available from kt's installed packages (kt-biome…). +export const listKohakuCreatures = (command?: string): Promise => { + return invoke('list_kohaku_creatures', { command: command ?? null }); +}; + +// One-click install of the official kt-biome creature pack. +export const kohakuInstallBiome = (command?: string): Promise => { + return invoke('kohaku_install_biome', { command: command ?? null }); +}; + export const runTurn = ( sessionId: string, message: string, diff --git a/crates/switchyard-gui/src/main.rs b/crates/switchyard-gui/src/main.rs index a0ed20e..936eaf6 100644 --- a/crates/switchyard-gui/src/main.rs +++ b/crates/switchyard-gui/src/main.rs @@ -3293,6 +3293,100 @@ async fn list_kohaku_models(command: Option) -> Result, Stri Ok(models) } +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct KohakuCreature { + /// `@/creatures/` ref to pass as the kt agent_path. + reference: String, + package: String, + name: String, + /// "creature" or "terrarium". + kind: String, +} + +/// List the creatures/terrariums available from KohakuTerrarium's installed +/// packages (kt-biome / kohaku-creatures, the ecosystem YAML agents) by +/// parsing `kt list`, so the GUI can offer them as the kt backend's agent +/// instead of hand-typing a `@pkg/creatures/` ref. +#[tauri::command] +async fn list_kohaku_creatures(command: Option) -> Result, String> { + let cmd = command + .map(|c| c.trim().to_string()) + .filter(|c| !c.is_empty()) + .unwrap_or_else(|| "kt".to_string()); + let mut process = tokio::process::Command::new(&cmd); + process.arg("list"); + rg_no_window(&mut process); + let Ok(output) = process.output().await else { + return Ok(Vec::new()); + }; + let stdout = String::from_utf8_lossy(&output.stdout); + let mut out = Vec::new(); + let mut pkg = String::new(); + for line in stdout.lines() { + if line.starts_with(" ") { + // Detail line (4-space indent): Creatures:/Terrariums: lists. + let trimmed = line.trim(); + let (rest, kind, segment) = if let Some(r) = trimmed.strip_prefix("Creatures:") { + (Some(r), "creature", "creatures") + } else if let Some(r) = trimmed.strip_prefix("Terrariums:") { + (Some(r), "terrarium", "terrariums") + } else { + (None, "", "") + }; + if let Some(rest) = rest { + if pkg.is_empty() { + continue; + } + for name in rest.split(',').map(str::trim).filter(|s| !s.is_empty()) { + out.push(KohakuCreature { + reference: format!("@{pkg}/{segment}/{name}"), + package: pkg.clone(), + name: name.to_string(), + kind: kind.to_string(), + }); + } + } + } else if line.starts_with(" ") { + // Package header (2-space indent): " vX.Y.Z". + if let Some((name, ver)) = line.trim().split_once(' ') { + if ver.starts_with('v') { + pkg = name.to_string(); + } + } + } + } + Ok(out) +} + +/// One-click install of the official kt-biome creature pack (`kt install +/// @kt-biome`). Returns the CLI output; errors carry stderr for display. +#[tauri::command] +async fn kohaku_install_biome(command: Option) -> Result { + let cmd = command + .map(|c| c.trim().to_string()) + .filter(|c| !c.is_empty()) + .unwrap_or_else(|| "kt".to_string()); + let mut process = tokio::process::Command::new(&cmd); + process.arg("install").arg("@kt-biome"); + rg_no_window(&mut process); + let output = process + .output() + .await + .map_err(|e| format!("run `{cmd} install @kt-biome`: {e}"))?; + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) + } else { + Err(format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) + .trim() + .to_string()) + } +} + #[tauri::command] async fn list_sessions( workspace_state: tauri::State<'_, WorkspaceState>, @@ -5069,6 +5163,8 @@ fn main() { load_config, save_config, list_kohaku_models, + list_kohaku_creatures, + kohaku_install_biome, list_provider_status_quick, list_provider_status, list_sessions,