WIP push-to-talk Letta chat frontend
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor letta settings

+255 -215
+5
deno.lock
··· 4 4 "jsr:@std/async@^1.0.14": "1.0.14", 5 5 "npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@6.3.5__picomatch@4.0.3", 6 6 "npm:@fontsource-variable/recursive@^5.2.7": "5.2.7", 7 + "npm:@fontsource/flow-block@^5.2.7": "5.2.7", 7 8 "npm:@iconify/json@^2.2.382": "2.2.382", 8 9 "npm:@iconify/tailwind4@^1.0.6": "1.0.6_tailwindcss@4.1.12", 9 10 "npm:@sveltejs/adapter-static@^3.0.6": "3.0.9_@sveltejs+kit@2.36.2__@sveltejs+vite-plugin-svelte@5.1.1___svelte@5.38.3____acorn@8.15.0___vite@6.3.5____picomatch@4.0.3__svelte@5.38.3___acorn@8.15.0__vite@6.3.5___picomatch@4.0.3__acorn@8.15.0_@sveltejs+vite-plugin-svelte@5.1.1__svelte@5.38.3___acorn@8.15.0__vite@6.3.5___picomatch@4.0.3_svelte@5.38.3__acorn@8.15.0_vite@6.3.5__picomatch@4.0.3", ··· 197 198 }, 198 199 "@fontsource-variable/recursive@5.2.7": { 199 200 "integrity": "sha512-6hDJ9EDUxShoqhmxV3B+q8k6YC7cBBzVvDapNAESDIn53SzBC7y1KbEr/aha70zx/GWJiNTTVf7gdJ3B5K1/vg==" 201 + }, 202 + "@fontsource/flow-block@5.2.7": { 203 + "integrity": "sha512-YpTYzbUo5HAIF4p5KAgw5oteN+d+TTyVCQzURR0Y+JIhVLI80qQ6Vl/AxUaDRPX5qwX6f+BhNJ3G8tUuMDAdSQ==" 200 204 }, 201 205 "@iconify/json@2.2.382": { 202 206 "integrity": "sha512-1UT0ouWPVXNteS+kaQjtDvxKy/swWqB84fq9b+xbpE7nhgfak7ljYneWSXTDU+SyfL112F9978p7Mf3C3Q/8LQ==", ··· 1130 1134 "dependencies": [ 1131 1135 "npm:@deno/vite-plugin@^1.0.5", 1132 1136 "npm:@fontsource-variable/recursive@^5.2.7", 1137 + "npm:@fontsource/flow-block@^5.2.7", 1133 1138 "npm:@iconify/json@^2.2.382", 1134 1139 "npm:@iconify/tailwind4@^1.0.6", 1135 1140 "npm:@sveltejs/adapter-static@^3.0.6",
+1
package.json
··· 15 15 "dependencies": { 16 16 "@deno/vite-plugin": "^1.0.5", 17 17 "@fontsource-variable/recursive": "^5.2.7", 18 + "@fontsource/flow-block": "^5.2.7", 18 19 "@iconify/json": "^2.2.382", 19 20 "@iconify/tailwind4": "^1.0.6", 20 21 "@tailwindcss/forms": "^0.5.10",
+35 -24
src-tauri/src/letta/commands.rs
··· 1 1 use crate::{ 2 - letta::{LettaAgentInfo, LettaConfig}, 2 + letta::{LettaAgentInfo, LettaConfigKey}, 3 3 state::AppState, 4 4 }; 5 5 ··· 9 9 } 10 10 11 11 #[tauri::command] 12 - pub async fn get_config(state: tauri::State<'_, AppState>) -> Result<LettaConfig, ()> { 13 - let config = state.letta_manager.config.lock().await; 12 + pub async fn get_letta_base_url(state: tauri::State<'_, AppState>) -> Result<String, ()> { 13 + Ok(state.letta_manager.base_url.lock().await.clone()) 14 + } 14 15 15 - Ok(config.clone()) 16 + #[tauri::command] 17 + pub async fn set_letta_base_url( 18 + state: tauri::State<'_, AppState>, 19 + url: String, 20 + ) -> Result<String, ()> { 21 + { 22 + let mut base_url = state.letta_manager.base_url.lock().await; 23 + *base_url = url.clone(); 24 + } 25 + 26 + state 27 + .letta_manager 28 + .store 29 + .set(LettaConfigKey::BaseUrl.to_string(), url.clone()); 30 + 31 + Ok(url) 16 32 } 17 33 18 34 #[tauri::command] 19 - pub async fn update_config( 35 + pub async fn get_letta_agent_id(state: tauri::State<'_, AppState>) -> Result<String, ()> { 36 + Ok(state.letta_manager.agent_id.lock().await.clone()) 37 + } 38 + 39 + #[tauri::command] 40 + pub async fn set_letta_agent_id( 20 41 state: tauri::State<'_, AppState>, 21 - base_url: Option<String>, 22 - agent_id: Option<String>, 23 - ) -> Result<(), ()> { 42 + id: String, 43 + ) -> Result<String, ()> { 24 44 { 25 - let mut config = state.letta_manager.config.lock().await; 26 - 27 - *config = LettaConfig { 28 - base_url: if base_url.is_some() { 29 - base_url.unwrap() 30 - } else { 31 - config.base_url.clone() 32 - }, 33 - agent_id: if agent_id.is_some() { 34 - agent_id.unwrap() 35 - } else { 36 - config.agent_id.clone() 37 - }, 38 - }; 45 + let mut agent_id = state.letta_manager.agent_id.lock().await; 46 + *agent_id = id.clone(); 39 47 } 40 48 41 - state.letta_manager.persist_config().await; 49 + state 50 + .letta_manager 51 + .store 52 + .set(LettaConfigKey::AgentId.to_string(), id.clone()); 42 53 43 - Ok(()) 54 + Ok(id) 44 55 }
+33 -34
src-tauri/src/letta/mod.rs
··· 2 2 3 3 use reqwest::Client; 4 4 use serde::{Deserialize, Serialize}; 5 - use serde_json::{from_value, json}; 5 + use serde_json::json; 6 + use strum::{Display, EnumString}; 6 7 use tauri::async_runtime::Mutex; 7 8 use tauri::Wry; 8 9 use tauri_plugin_store::Store; ··· 11 12 12 13 pub mod commands; 13 14 14 - #[derive(Serialize, Deserialize, Clone)] 15 - #[serde(rename_all = "camelCase")] 16 - pub struct LettaConfig { 17 - base_url: String, 18 - agent_id: String, 15 + #[derive(Serialize, Deserialize, Clone, Display, EnumString)] 16 + #[strum(prefix = "letta")] 17 + pub enum LettaConfigKey { 18 + BaseUrl, 19 + AgentId, 19 20 } 20 21 21 22 #[derive(Serialize, Deserialize)] ··· 28 29 http_client: Client, 29 30 secrets_manager: Arc<SecretsManager>, 30 31 store: Arc<Store<Wry>>, 31 - config: Arc<Mutex<LettaConfig>>, 32 + base_url: Mutex<String>, 33 + agent_id: Mutex<String>, 32 34 } 33 35 impl LettaManager { 34 36 pub fn new( ··· 36 38 store: Arc<Store<Wry>>, 37 39 secrets_manager: Arc<SecretsManager>, 38 40 ) -> Self { 39 - match store.get("letta-config") { 40 - Some(config) => Self { 41 - http_client, 42 - secrets_manager, 43 - store, 44 - config: Arc::new(Mutex::new( 45 - from_value::<LettaConfig>(config).expect("failed to deserialize Letta config"), 46 - )), 47 - }, 41 + let base_url = match store.get(LettaConfigKey::BaseUrl.to_string()) { 42 + Some(url) => url 43 + .as_str() 44 + .expect("Letta base URL in config is not a string") 45 + .to_owned(), 48 46 None => { 49 - let new_config = LettaConfig { 50 - base_url: "https://api.letta.com".to_string(), 51 - agent_id: "".to_string(), 52 - }; 47 + let url = "https://api.letta.com".to_owned(); 53 48 54 - store.set("letta-config", json!(new_config)); 49 + store.set(LettaConfigKey::BaseUrl.to_string(), json!(url)); 55 50 56 - Self { 57 - http_client, 58 - secrets_manager, 59 - store, 60 - config: Arc::new(Mutex::new(new_config)), 61 - } 51 + url 62 52 } 53 + }; 54 + let agent_id = match store.get(LettaConfigKey::AgentId.to_string()) { 55 + Some(id) => id 56 + .as_str() 57 + .expect("Letta agent ID in config is not a string") 58 + .to_owned(), 59 + None => "".to_owned(), 60 + }; 61 + 62 + Self { 63 + http_client, 64 + secrets_manager, 65 + store, 66 + base_url: Mutex::new(base_url), 67 + agent_id: Mutex::new(agent_id), 63 68 } 64 69 } 65 70 ··· 69 74 .get_secret(crate::secrets::SecretName::LettaApiKey) 70 75 { 71 76 Ok(api_key) => { 72 - let config = self.config.lock().await; 73 - let base_url = config.base_url.clone(); 77 + let base_url = self.base_url.lock().await.to_owned(); 74 78 let req = self 75 79 .http_client 76 80 .get(format!("{base_url}/v1/agents/")) ··· 86 90 } 87 91 Err(_) => Vec::new(), 88 92 } 89 - } 90 - 91 - pub async fn persist_config(&self) { 92 - self.store 93 - .set("letta-config", json!(*self.config.lock().await)); 94 93 } 95 94 }
+4 -2
src-tauri/src/lib.rs
··· 69 69 cartesia::commands::start_stt, 70 70 cartesia::commands::stop_stt, 71 71 letta::commands::list_agents, 72 - letta::commands::get_config, 73 - letta::commands::update_config, 72 + letta::commands::get_letta_base_url, 73 + letta::commands::set_letta_base_url, 74 + letta::commands::get_letta_agent_id, 75 + letta::commands::set_letta_agent_id, 74 76 secrets::commands::has_secret, 75 77 secrets::commands::set_secret, 76 78 secrets::commands::delete_secret,
+1
src/app.css
··· 20 20 --color-rose-pine-highlight-high: hsl(248deg, 13%, 36%); 21 21 22 22 --font-sans: "Recursive Variable"; 23 + --font-skeleton: "Flow Block"; 23 24 24 25 --animate-wiggle: wiggle 1s ease-in-out infinite; 25 26
+88 -70
src/lib/forms/dropdown.svelte
··· 1 - <script lang="ts"> 2 - import type { HTMLSelectAttributes } from "svelte/elements"; 3 - 4 - type DropdownProps = HTMLSelectAttributes & { 5 - label: string; 6 - initialOption?: string; 7 - options: DropdownOption[] | Promise<DropdownOption[]> 8 - onSelect: (value: string) => void | Promise<void> 9 - } 10 - 11 - interface DropdownOption { 12 - label: string 13 - value: string 14 - } 15 - 16 - let { 17 - label, 18 - initialOption, 19 - options, 20 - onSelect, 21 - required, 22 - ...props 23 - }: DropdownProps = $props(); 24 - 25 - let value = $state(initialOption) 26 - let isLoading = $derived(!Array.isArray(options)) 27 - 28 - $effect(() => { 29 - if (isLoading && !Array.isArray(options)) { 30 - options.then(() => isLoading = false) 31 - } 32 - }) 33 - </script> 34 - 35 - <label class="flex flex-col gap-0.5 w-full"> 36 - <span class="text-sm text-rose-pine-subtle">{label}</span> 37 - <div 38 - class="flex gap-2" 39 - > 40 - <select 41 - {...props} 42 - bind:value 43 - class={[ 44 - "bg-rose-pine-surface px-3 py-2 w-full flex-1 border-1 border-rose-pine-muted/20 rounded-sm focus:ring-2 ring-rose-pine-iris text-rose-pine-text placeholder:text-rose-pine-subtle", 45 - props.class, 46 - isLoading && "animate-pulse" 47 - ]} 48 - onchange={e => onSelect(e.currentTarget.value)} 49 - > 50 - {#await Promise.resolve(options) then opts} 51 - {#each opts as {value, label}} 52 - <option {value}>{label}</option> 53 - {/each} 54 - {/await} 55 - </select> 56 - {#if !value} 57 - <div class="flex items-center justify-center"> 58 - <span 59 - class="icon-[tabler--exclamation-circle] size-6 text-rose-pine-love" 60 - ></span> 61 - </div> 62 - {:else} 63 - <div class="flex items-center justify-center"> 64 - <span 65 - class="icon-[tabler--circle-check-filled] size-6 text-rose-pine-foam" 66 - ></span> 67 - </div> 68 - {/if} 69 - </div> 70 - </label> 1 + <script lang="ts"> 2 + import type { HTMLSelectAttributes } from "svelte/elements"; 3 + 4 + type DropdownProps = HTMLSelectAttributes & { 5 + label: string; 6 + initialOption: string | Promise<string>; 7 + options: DropdownOption[] | Promise<DropdownOption[]>; 8 + onSelect: (value: string) => void | Promise<void>; 9 + }; 10 + 11 + interface DropdownOption { 12 + label: string; 13 + value: string; 14 + } 15 + 16 + let { 17 + label, 18 + initialOption, 19 + options, 20 + onSelect, 21 + required, 22 + ...props 23 + }: DropdownProps = $props(); 24 + 25 + let value = $state("loading"); 26 + let err = $state("") 27 + let isLoading = $derived(!Array.isArray(options)); 28 + 29 + $effect(() => { 30 + Promise.allSettled([ 31 + Promise.resolve(initialOption), 32 + Promise.resolve(options) 33 + ]).then(([init, opts]) => { 34 + if (init.status === "fulfilled") { 35 + value = init.value 36 + } 37 + 38 + if (opts.status === "rejected") { 39 + err = opts.reason 40 + } 41 + 42 + isLoading = false 43 + }) 44 + }); 45 + </script> 46 + 47 + <label class="flex flex-col gap-0.5 w-full"> 48 + <span class="text-sm text-rose-pine-subtle">{label}</span> 49 + <div 50 + class="flex gap-2" 51 + > 52 + <select 53 + {...props} 54 + bind:value 55 + class={[ 56 + "bg-rose-pine-surface px-3 py-2 w-full flex-1 border-1 border-rose-pine-muted/20 rounded-sm focus:ring-2 ring-rose-pine-iris text-rose-pine-text placeholder:text-rose-pine-subtle", 57 + isLoading && "font-skeleton animate-pulse", props.class, 58 + 59 + ]} 60 + onchange={(e) => onSelect(e.currentTarget.value)} 61 + > 62 + {#await Promise.resolve(options)} 63 + <option value="loading">Loading...</option> 64 + {:then opts} 65 + {#each opts as { value, label }} 66 + <option {value}>{label}</option> 67 + {/each} 68 + {/await} 69 + </select> 70 + {#if required || isLoading} 71 + <div class="flex items-center justify-center"> 72 + <span 73 + class={[ 74 + "size-6", 75 + isLoading 76 + ? "icon-[svg-spinners--90-ring-with-bg] text-rose-pine-text" 77 + : value 78 + ? "icon-[tabler--circle-check-filled] text-rose-pine-foam" 79 + : "icon-[tabler--exclamation-circle] text-rose-pine-love" 80 + ]} 81 + ></span> 82 + </div> 83 + {/if} 84 + </div> 85 + {#if err} 86 + <p class="mt-1 text-rose-pine-love">{err}</p> 87 + {/if} 88 + </label>
+30 -32
src/lib/forms/input.svelte
··· 1 1 <script lang="ts"> 2 2 import type { HTMLInputAttributes } from "svelte/elements"; 3 - import { crossfade, fly } from "svelte/transition"; 4 3 5 4 type InputProps = HTMLInputAttributes & { 6 5 label: string; 7 - initialValue: string; 6 + initialValue: string | Promise<string>; 8 7 onSave: (value: string) => void | Promise<void>; 9 8 }; 10 9 ··· 16 15 ...props 17 16 }: InputProps = $props(); 18 17 19 - let value = $state(initialValue); 20 - let isDirty = $derived(value !== initialValue); 21 - let savePromise: Promise<void> | undefined = $state(); 18 + let value = $state("loading"); 19 + let initial = $state("loading"); 20 + let isDirty = $derived(value !== initial); 21 + let isLoading = $state(typeof initialValue === "object" && "then" in initialValue) 22 22 23 - function onclick() { 24 - savePromise = Promise.resolve(onSave(value)); 23 + $effect(() => { 24 + Promise.resolve(initialValue) 25 + .then(str => {value = str; initial = str; isLoading = false}) 26 + }) 27 + 28 + async function onsubmit() { 29 + isLoading = true 30 + await Promise.resolve(onSave(value)); 31 + isLoading = false 25 32 } 26 - 27 - $inspect(value); 28 33 </script> 29 34 30 35 <label class="flex flex-col gap-0.5"> 31 36 <span class="text-sm text-rose-pine-subtle">{label}</span> 32 - <div 37 + <form 33 38 class="flex gap-2" 39 + {onsubmit} 34 40 > 35 41 <input 36 42 {...props} 43 + disabled={props.disabled || isLoading} 37 44 bind:value 38 45 class={[ 39 46 "peer block bg-rose-pine-surface px-3 py-2 flex-1 border-1 rounded-sm border-rose-pine-muted/20 focus:ring-2 ring-rose-pine-iris text-rose-pine-text placeholder:text-rose-pine-subtle", 47 + isLoading && "font-skeleton animate-pulse", 40 48 props.class, 41 49 ]} 42 50 /> 43 51 {#if isDirty} 44 52 <button 45 53 class={[ 46 - "flex items-center justify-center gap-3 py-2 px-4 font-bold rounded-md transition disabled:cursor-not-allowed border-l-2 border-rose-pine-base uppercase text-sm", 54 + "flex items-center justify-center gap-3 py-2 px-4 font-black tracking-wider rounded-md transition disabled:cursor-not-allowed border-l-2 border-rose-pine-base uppercase text-sm", 47 55 "bg-rose-pine-gold text-rose-pine-highlight-low peer-focus:bg-rose-pine-foam focus:bg-rose-pine-foam", 48 56 ]} 49 57 disabled={!isDirty} 50 - {onclick} 51 58 aria-label={`Save changes to ${label}`} 52 59 > 53 - {#if savePromise} 54 - {#await savePromise} 55 - loading 56 - {:then _} 57 - saved 58 - {/await} 59 - {:else} 60 - save 61 - {/if} 60 + save 62 61 </button> 63 - {:else if required && !value} 62 + {:else if required || isLoading} 64 63 <div class="flex items-center justify-center"> 65 - <span 66 - class="icon-[tabler--exclamation-circle-filled] size-6 text-rose-pine-love" 67 - ></span> 68 - </div> 69 - {:else if required && initialValue} 70 - <div class="flex items-center justify-center"> 71 - <span 72 - class="icon-[tabler--circle-check-filled] size-6 text-rose-pine-foam" 73 - ></span> 64 + <span class={[ 65 + "size-6", 66 + isLoading 67 + ? "icon-[svg-spinners--90-ring-with-bg] text-rose-pine-text" 68 + : value 69 + ? "icon-[tabler--circle-check-filled] text-rose-pine-foam" 70 + : "icon-[tabler--exclamation-circle-filled] text-rose-pine-love" 71 + ]}></span> 74 72 </div> 75 73 {/if} 76 - </div> 74 + </form> 77 75 </label>
+1
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 2 import "@fontsource-variable/recursive/full.css"; 3 + import "@fontsource/flow-block/400.css" 3 4 import "../app.css"; 4 5 5 6 let { children } = $props();
+57 -53
src/routes/settings/letta-settings.svelte
··· 11 11 async function deleteKey() { 12 12 await invoke("delete_secret", { name: "letta_api_key" }); 13 13 14 - lettaKeyGuard = hasLettaKey(); 14 + hasApiKey = hasLettaKey(); 15 15 } 16 16 17 - interface LettaConfig { 18 - baseUrl: string; 19 - agentId: string; 17 + interface LettaAgentOption { 18 + name: string; 19 + id: string; 20 20 } 21 21 22 - interface LettaAgentOption { 23 - name: string 24 - id: string 22 + function fetchBaseUrl() { 23 + return invoke<string>("get_letta_base_url"); 25 24 } 26 25 27 - function getLettaConfig(): Promise<LettaConfig> { 28 - return invoke("get_config"); 26 + function fetchAgentId() { 27 + return invoke<string>("get_letta_agent_id"); 29 28 } 30 29 31 30 function getLettaAgents(): Promise<LettaAgentOption[]> { 32 - return invoke("list_agents") 31 + return invoke("list_agents"); 33 32 } 34 33 35 - let lettaKeyGuard = $state(hasLettaKey()); 36 - let lettaConfig = $state(getLettaConfig()); 37 - let lettaAgents = $state(getLettaAgents()) 34 + let hasApiKey = $state(hasLettaKey()); 35 + 36 + let baseUrl = $state(fetchBaseUrl()); 37 + let agentId = $state(fetchAgentId()); 38 + 39 + let lettaAgents: Promise<LettaAgentOption[]> = $state(Promise.resolve([])); 38 40 39 - let showAgentSettings = $derived.by(async () => { 40 - const [hasKey, config] = await Promise.all([ 41 - lettaKeyGuard, 42 - lettaConfig, 41 + let allowAgentEdit = $derived.by(async () => { 42 + const [hasKey, url] = await Promise.all([ 43 + hasApiKey, 44 + baseUrl, 43 45 ]); 44 46 45 - return Boolean(hasKey && config.baseUrl); 47 + const canEdit = Boolean(hasKey && url); 48 + 49 + if (canEdit) lettaAgents = getLettaAgents() 50 + 51 + return canEdit 46 52 }); 47 53 </script> 48 54 ··· 51 57 > 52 58 <h2 class="text-xl font-bold">Letta Settings</h2> 53 59 54 - {#await lettaKeyGuard then hasKey} 60 + {#await hasApiKey then hasKey} 55 61 {#if hasKey} 56 62 <div 57 - class="px-3 py-2 rounded-md grid grid-flow-col grid-cols-[auto_auto_1fr] grid-rows-1 bg-rose-pine-pine items-center gap-3" 63 + class="p-3 rounded-md grid grid-flow-col grid-cols-[auto_auto_1fr] grid-rows-1 bg-rose-pine-pine items-center gap-3" 58 64 > 59 - <span class="icon-[tabler--circle-check-filled] size-5"></span> 65 + <span class="icon-[tabler--circle-check-filled] size-6"></span> 60 66 <p>Letta key is set!</p> 61 67 <button 62 68 onclick={deleteKey} ··· 77 83 value: key, 78 84 }); 79 85 80 - lettaKeyGuard = hasLettaKey(); 86 + hasApiKey = hasLettaKey(); 81 87 }} 82 88 /> 83 89 {/if} ··· 85 91 <p>{err}</p> 86 92 {/await} 87 93 88 - {#await lettaConfig then { baseUrl, agentId }} 89 - <Input 90 - label="Base URL" 91 - placeholder="https://api.letta.com" 92 - initialValue={baseUrl} 93 - required 94 - onSave={async (url) => { 95 - await invoke("update_config", { baseUrl: url }); 96 - lettaConfig = getLettaConfig(); 97 - }} 98 - /> 99 - {#await showAgentSettings then showSettings} 100 - {#if showSettings} 101 - <Dropdown 102 - label="Agent ID" 103 - initialOption={agentId} 104 - options={lettaAgents.then(agents => agents.map(agent => ({ label: agent.name, value: agent.id})))} 105 - onSelect={async id => { 106 - console.log(id) 107 - await invoke("update_config", { agentId: id }) 108 - lettaConfig = getLettaConfig() 109 - }} 110 - /> 111 - {/if} 112 - {/await} 113 - {/await} 94 + <Input 95 + label="Base URL" 96 + placeholder="https://api.letta.com" 97 + initialValue={baseUrl} 98 + required 99 + onSave={async (url) => { 100 + await invoke("set_letta_base_url", { url }); 101 + baseUrl = fetchBaseUrl(); 102 + }} 103 + /> 114 104 115 - {#await showAgentSettings} 116 - <div class="flex items-center justify-center"> 105 + {#await allowAgentEdit} 106 + <div class="flex items-center justify-center p-3"> 117 107 <span 118 108 class="icon-[svg-spinners--180-ring-with-bg] text-rose-pine-muted" 119 109 ></span> 120 110 </div> 121 111 {:then showSettings} 122 - {#if !showSettings} 112 + {#if showSettings} 113 + {#await agentId then id} 114 + <Dropdown 115 + label="Agent" 116 + initialOption={id} 117 + required 118 + options={lettaAgents.then((agents) => 119 + agents.map((agent) => ({ label: agent.name, value: agent.id })) 120 + )} 121 + onSelect={async (id) => { 122 + await invoke("set_letta_agent_id", { id }); 123 + agentId = fetchAgentId(); 124 + }} 125 + /> 126 + {/await} 127 + {:else} 123 128 <Warning> 124 - You must set your API key and base URL before managing other Letta 125 - settings 129 + You must set your API key and base URL before selecting an agent 126 130 </Warning> 127 131 {/if} 128 132 {/await}