From d7b45125e93635301b5b8cbec48efd01fe381c39 Mon Sep 17 00:00:00 2001 From: Wes Date: Mon, 16 Mar 2026 09:03:43 -0700 Subject: [PATCH] Simplify create agent setup flow (#78) --- desktop/src-tauri/src/commands/agents.rs | 296 ++++++++++-------- desktop/src-tauri/src/managed_agents/types.rs | 1 + desktop/src-tauri/src/relay.rs | 77 ++++- .../features/agents/ui/CreateAgentDialog.tsx | 140 +++++---- .../agents/ui/CreateAgentDialogSections.tsx | 178 ++++++----- .../features/agents/ui/SecretRevealDialog.tsx | 6 + desktop/src/features/agents/ui/agentUi.ts | 14 - .../src/features/sidebar/ui/AppSidebar.tsx | 3 + .../src/features/tokens/lib/scopeOptions.ts | 31 ++ .../features/tokens/ui/TokenSettingsCard.tsx | 13 +- desktop/src/shared/api/tauri.ts | 2 + desktop/src/shared/api/types.ts | 1 + 12 files changed, 463 insertions(+), 299 deletions(-) create mode 100644 desktop/src/features/tokens/lib/scopeOptions.ts diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index 37d350435b..ef2654851d 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -15,7 +15,10 @@ use crate::{ RelayAgentInfo, DEFAULT_ACP_COMMAND, DEFAULT_AGENT_ARG, DEFAULT_AGENT_COMMAND, DEFAULT_AGENT_TURN_TIMEOUT_SECONDS, DEFAULT_MCP_COMMAND, }, - relay::{build_authed_request, managed_agent_owner_pubkey, relay_ws_url, send_json_request}, + relay::{ + build_authed_request, managed_agent_owner_pubkey, relay_ws_url, send_json_request, + sync_managed_agent_profile_display_name, + }, util::now_iso, }; @@ -98,145 +101,176 @@ pub async fn create_managed_agent( None }; - let _store_guard = state - .managed_agents_store_lock - .lock() - .map_err(|error| error.to_string())?; - let mut records = load_managed_agents(&app)?; - let mut runtimes = state - .managed_agent_processes - .lock() - .map_err(|error| error.to_string())?; + let (agent, private_key_nsec, api_token, pubkey, resolved_relay_url, spawn_error, token_scopes) = + { + let _store_guard = state + .managed_agents_store_lock + .lock() + .map_err(|error| error.to_string())?; + let mut records = load_managed_agents(&app)?; + let mut runtimes = state + .managed_agent_processes + .lock() + .map_err(|error| error.to_string())?; - if sync_managed_agent_processes(&mut records, &mut runtimes) { - save_managed_agents(&app, &records)?; - } + if sync_managed_agent_processes(&mut records, &mut runtimes) { + save_managed_agents(&app, &records)?; + } - let keys = Keys::generate(); - let pubkey = keys.public_key().to_hex(); - if records.iter().any(|record| record.pubkey == pubkey) { - return Err(format!("agent {pubkey} already exists")); - } + let keys = Keys::generate(); + let pubkey = keys.public_key().to_hex(); + if records.iter().any(|record| record.pubkey == pubkey) { + return Err(format!("agent {pubkey} already exists")); + } - let private_key_nsec = keys - .secret_key() - .to_bech32() - .map_err(|error| format!("failed to encode private key: {error}"))?; - let token_scopes = if input.mint_token { - let requested = input - .token_scopes - .into_iter() - .map(|scope| scope.trim().to_string()) - .filter(|scope| !scope.is_empty()) - .collect::>(); - if requested.is_empty() { - default_token_scopes() - } else { - requested - } - } else { - Vec::new() - }; - let minted = if input.mint_token { - let token_name = input - .token_name - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .unwrap_or(name); - Some(run_sprout_admin_mint_token( - &app, - &pubkey, - owner_pubkey + let private_key_nsec = keys + .secret_key() + .to_bech32() + .map_err(|error| format!("failed to encode private key: {error}"))?; + let token_scopes = if input.mint_token { + let requested = input + .token_scopes + .into_iter() + .map(|scope| scope.trim().to_string()) + .filter(|scope| !scope.is_empty()) + .collect::>(); + if requested.is_empty() { + default_token_scopes() + } else { + requested + } + } else { + Vec::new() + }; + let api_token = if input.mint_token { + let token_name = input + .token_name + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or(name); + Some( + run_sprout_admin_mint_token( + &app, + &pubkey, + owner_pubkey.as_deref().ok_or_else(|| { + "managed agent owner pubkey was not resolved".to_string() + })?, + token_name, + &token_scopes, + )? + .api_token, + ) + } else { + None + }; + let resolved_relay_url = input + .relay_url .as_deref() - .ok_or_else(|| "managed agent owner pubkey was not resolved".to_string())?, - token_name, - &token_scopes, - )?) - } else { - None + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string) + .unwrap_or_else(relay_ws_url); + + let mut record = crate::managed_agents::ManagedAgentRecord { + pubkey: pubkey.clone(), + name: name.to_string(), + private_key_nsec: private_key_nsec.clone(), + api_token: api_token.clone(), + relay_url: resolved_relay_url.clone(), + acp_command: input + .acp_command + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or(DEFAULT_ACP_COMMAND) + .to_string(), + agent_command: input + .agent_command + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or(DEFAULT_AGENT_COMMAND) + .to_string(), + agent_args: input + .agent_args + .into_iter() + .map(|arg| arg.trim().to_string()) + .filter(|arg| !arg.is_empty()) + .collect::>(), + mcp_command: input + .mcp_command + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or(DEFAULT_MCP_COMMAND) + .to_string(), + turn_timeout_seconds: input + .turn_timeout_seconds + .filter(|seconds| *seconds > 0) + .unwrap_or(DEFAULT_AGENT_TURN_TIMEOUT_SECONDS), + created_at: now_iso(), + updated_at: now_iso(), + last_started_at: None, + last_stopped_at: None, + last_exit_code: None, + last_error: None, + }; + + if record.agent_args.is_empty() { + record.agent_args.push(DEFAULT_AGENT_ARG.to_string()); + } + + records.push(record); + + let mut spawn_error = None; + if input.spawn_after_create { + let record = find_managed_agent_mut(&mut records, &pubkey)?; + if let Err(error) = start_managed_agent_process(&app, record, &mut runtimes) { + record.updated_at = now_iso(); + record.last_error = Some(error.clone()); + spawn_error = Some(error); + } + } + + save_managed_agents(&app, &records)?; + + let record = records + .iter() + .find(|record| record.pubkey == pubkey) + .ok_or_else(|| "created agent disappeared unexpectedly".to_string())?; + let agent = build_managed_agent_summary(&app, record, &runtimes)?; + + Ok::<_, String>(( + agent, + private_key_nsec, + api_token, + pubkey, + resolved_relay_url, + spawn_error, + token_scopes, + )) + }?; + + let profile_sync_error = match sync_managed_agent_profile_display_name( + &state, + &resolved_relay_url, + &pubkey, + api_token.as_deref(), + &token_scopes, + name, + ) + .await + { + Ok(()) => None, + Err(error) => Some(error), }; - let resolved_relay_url = input - .relay_url - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(str::to_string) - .unwrap_or_else(relay_ws_url); - - let mut record = crate::managed_agents::ManagedAgentRecord { - pubkey: pubkey.clone(), - name: name.to_string(), - private_key_nsec: private_key_nsec.clone(), - api_token: minted.as_ref().map(|output| output.api_token.clone()), - relay_url: resolved_relay_url, - acp_command: input - .acp_command - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .unwrap_or(DEFAULT_ACP_COMMAND) - .to_string(), - agent_command: input - .agent_command - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .unwrap_or(DEFAULT_AGENT_COMMAND) - .to_string(), - agent_args: input - .agent_args - .into_iter() - .map(|arg| arg.trim().to_string()) - .filter(|arg| !arg.is_empty()) - .collect::>(), - mcp_command: input - .mcp_command - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .unwrap_or(DEFAULT_MCP_COMMAND) - .to_string(), - turn_timeout_seconds: input - .turn_timeout_seconds - .filter(|seconds| *seconds > 0) - .unwrap_or(DEFAULT_AGENT_TURN_TIMEOUT_SECONDS), - created_at: now_iso(), - updated_at: now_iso(), - last_started_at: None, - last_stopped_at: None, - last_exit_code: None, - last_error: None, - }; - - if record.agent_args.is_empty() { - record.agent_args.push(DEFAULT_AGENT_ARG.to_string()); - } - - records.push(record); - - let mut spawn_error = None; - if input.spawn_after_create { - let record = find_managed_agent_mut(&mut records, &pubkey)?; - if let Err(error) = start_managed_agent_process(&app, record, &mut runtimes) { - record.updated_at = now_iso(); - record.last_error = Some(error.clone()); - spawn_error = Some(error); - } - } - - save_managed_agents(&app, &records)?; - - let record = records - .iter() - .find(|record| record.pubkey == pubkey) - .ok_or_else(|| "created agent disappeared unexpectedly".to_string())?; - let agent = build_managed_agent_summary(&app, record, &runtimes)?; Ok(CreateManagedAgentResponse { agent, private_key_nsec, - api_token: minted.map(|output| output.api_token), + api_token, + profile_sync_error, spawn_error, }) } diff --git a/desktop/src-tauri/src/managed_agents/types.rs b/desktop/src-tauri/src/managed_agents/types.rs index 550a456d57..374b60ae1d 100644 --- a/desktop/src-tauri/src/managed_agents/types.rs +++ b/desktop/src-tauri/src/managed_agents/types.rs @@ -85,6 +85,7 @@ pub struct CreateManagedAgentResponse { pub agent: ManagedAgentSummary, pub private_key_nsec: String, pub api_token: Option, + pub profile_sync_error: Option, pub spawn_error: Option, } diff --git a/desktop/src-tauri/src/relay.rs b/desktop/src-tauri/src/relay.rs index b79aa647a7..24325923e0 100644 --- a/desktop/src-tauri/src/relay.rs +++ b/desktop/src-tauri/src/relay.rs @@ -4,20 +4,35 @@ use reqwest::Method; use serde::de::DeserializeOwned; use sha2::{Digest, Sha256}; -use crate::{app_state::AppState, models::ProfileInfo}; +use crate::{ + app_state::AppState, + models::{ProfileInfo, UpdateProfileBody}, +}; pub fn relay_ws_url() -> String { std::env::var("SPROUT_RELAY_URL").unwrap_or_else(|_| "ws://localhost:3000".to_string()) } -pub fn relay_api_base_url() -> String { - if let Ok(base) = std::env::var("SPROUT_RELAY_HTTP") { - return base; +pub fn relay_http_base_url(relay_url: &str) -> String { + let trimmed = relay_url.trim().trim_end_matches('/'); + + if let Some(suffix) = trimmed.strip_prefix("wss://") { + return format!("https://{suffix}"); } - relay_ws_url() - .replace("wss://", "https://") - .replace("ws://", "http://") + if let Some(suffix) = trimmed.strip_prefix("ws://") { + return format!("http://{suffix}"); + } + + trimmed.to_string() +} + +pub fn relay_api_base_url() -> String { + if let Ok(base) = std::env::var("SPROUT_RELAY_HTTP") { + return base.trim().trim_end_matches('/').to_string(); + } + + relay_http_base_url(&relay_ws_url()) } pub fn build_authed_request( @@ -62,6 +77,54 @@ pub async fn managed_agent_owner_pubkey(state: &AppState) -> Result bool { + scopes.iter().any(|scope| scope == required_scope) +} + +pub async fn sync_managed_agent_profile_display_name( + state: &AppState, + relay_url: &str, + pubkey: &str, + api_token: Option<&str>, + token_scopes: &[String], + display_name: &str, +) -> Result<(), String> { + let url = format!( + "{}{}", + relay_http_base_url(relay_url), + "/api/users/me/profile" + ); + let use_bearer_token = api_token.is_some() && token_supports_scope(token_scopes, "users:write"); + let mut request = state.http_client.request(Method::PUT, url); + + if let Some(token) = api_token.filter(|_| use_bearer_token) { + request = request.header("Authorization", format!("Bearer {token}")); + } else { + request = request.header("X-Pubkey", pubkey); + } + + let request = request.json(&UpdateProfileBody { + display_name: Some(display_name), + avatar_url: None, + about: None, + nip05_handle: None, + }); + + send_empty_request(request).await.map_err(|error| { + if api_token.is_some() && !use_bearer_token { + format!( + "Created the agent, but could not sync its profile display name. The minted token does not include `users:write`, and the relay rejected dev-mode pubkey auth: {error}" + ) + } else if api_token.is_some() { + format!("Created the agent, but could not sync its profile display name: {error}") + } else { + format!( + "Created the agent, but could not sync its profile display name without a token: {error}" + ) + } + }) +} + fn session_api_token(state: &AppState) -> Result, String> { let token = state .session_token diff --git a/desktop/src/features/agents/ui/CreateAgentDialog.tsx b/desktop/src/features/agents/ui/CreateAgentDialog.tsx index d44e3021fb..4254a55ef0 100644 --- a/desktop/src/features/agents/ui/CreateAgentDialog.tsx +++ b/desktop/src/features/agents/ui/CreateAgentDialog.tsx @@ -1,3 +1,4 @@ +import { ChevronDown } from "lucide-react"; import * as React from "react"; import { @@ -5,6 +6,7 @@ import { useCreateManagedAgentMutation, useManagedAgentPrereqsQuery, } from "@/features/agents/hooks"; +import { DEFAULT_MANAGED_AGENT_SCOPES } from "@/features/tokens/lib/scopeOptions"; import type { CreateManagedAgentInput, CreateManagedAgentResponse, @@ -22,6 +24,7 @@ import { CreateAgentBasicsFields, CreateAgentOptionToggles, CreateAgentPrerequisitesCard, + CreateAgentRuntimeProviderField, CreateAgentRuntimeFields, CreateAgentTokenSection, } from "./CreateAgentDialogSections"; @@ -48,20 +51,14 @@ export function CreateAgentDialog({ const [spawnAfterCreate, setSpawnAfterCreate] = React.useState(true); const [tokenName, setTokenName] = React.useState(""); const [selectedScopes, setSelectedScopes] = React.useState>( - () => - new Set([ - "messages:read", - "messages:write", - "channels:read", - "users:read", - "users:write", - ]), + () => new Set(DEFAULT_MANAGED_AGENT_SCOPES), ); const [turnTimeoutSeconds, setTurnTimeoutSeconds] = React.useState("300"); const [selectedProviderId, setSelectedProviderId] = React.useState("custom"); const [hasSyncedProviderSelection, setHasSyncedProviderSelection] = React.useState(false); + const [showAdvanced, setShowAdvanced] = React.useState(false); const providers = providersQuery.data ?? []; const prereqs = prereqsQuery.data ?? null; const selectedProvider = React.useMemo( @@ -137,21 +134,22 @@ export function CreateAgentDialog({ setSpawnAfterCreate(false); }, [prereqs, spawnAfterCreate]); + React.useEffect(() => { + if ( + providersQuery.error instanceof Error || + prereqsQuery.error instanceof Error + ) { + setShowAdvanced(true); + } + }, [prereqsQuery.error, providersQuery.error]); + function reset() { setName(""); setRelayUrl(""); setMintToken(true); setSpawnAfterCreate(true); setTokenName(""); - setSelectedScopes( - new Set([ - "messages:read", - "messages:write", - "channels:read", - "users:read", - "users:write", - ]), - ); + setSelectedScopes(new Set(DEFAULT_MANAGED_AGENT_SCOPES)); setAcpCommand("sprout-acp"); setAgentCommand("goose"); setAgentArgs("acp"); @@ -159,6 +157,7 @@ export function CreateAgentDialog({ setTurnTimeoutSeconds("300"); setSelectedProviderId("custom"); setHasSyncedProviderSelection(false); + setShowAdvanced(false); createMutation.reset(); } @@ -186,6 +185,7 @@ export function CreateAgentDialog({ setSelectedProviderId(nextProviderId); if (nextProviderId === "custom") { + setShowAdvanced(true); return; } @@ -244,55 +244,23 @@ export function CreateAgentDialog({ Create agent - This creates a local agent identity, optionally mints a relay - token, and can spawn `sprout-acp` immediately. + This creates a local agent identity, syncs its display name when + possible, optionally mints a relay token, and can spawn + `sprout-acp` immediately.
- + - - - - {providersQuery.error instanceof Error ? ( -

- {providersQuery.error.message} -

- ) : null} - - {prereqsQuery.error instanceof Error ? ( -

- {prereqsQuery.error.message} -

- ) : null} - ) : null} +
+ + + {showAdvanced ? ( +
+
+ + + + + {providersQuery.error instanceof Error ? ( +

+ {providersQuery.error.message} +

+ ) : null} + + {prereqsQuery.error instanceof Error ? ( +

+ {prereqsQuery.error.message} +

+ ) : null} +
+
+ ) : null} +
+ {createMutation.error instanceof Error ? (

{createMutation.error.message} diff --git a/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx b/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx index ec60d71527..abde304d4e 100644 --- a/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx +++ b/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx @@ -4,9 +4,10 @@ import type { ManagedAgentPrereqs, TokenScope, } from "@/shared/api/types"; +import { MANAGED_AGENT_SCOPE_OPTIONS } from "@/features/tokens/lib/scopeOptions"; import { cn } from "@/shared/lib/cn"; import { Input } from "@/shared/ui/input"; -import { AGENT_SCOPE_OPTIONS, describeResolvedCommand } from "./agentUi"; +import { describeResolvedCommand } from "./agentUi"; export type PrerequisiteCard = { id: string; @@ -17,41 +18,85 @@ export type PrerequisiteCard = { export function CreateAgentBasicsFields({ name, - relayUrl, onNameChange, - onRelayUrlChange, }: { name: string; - relayUrl: string; onNameChange: (value: string) => void; - onRelayUrlChange: (value: string) => void; }) { return ( -

-
- - onNameChange(event.target.value)} - placeholder="alice" - value={name} - /> -
+
+ + onNameChange(event.target.value)} + placeholder="Support bot" + spellCheck={false} + value={name} + /> +

+ Used as the local label and synced to the agent profile display name + when the relay accepts the create-time auth. +

+
+ ); +} -
- - onRelayUrlChange(event.target.value)} - placeholder="Leave blank to use the desktop relay" - value={relayUrl} - /> -
+export function CreateAgentRuntimeProviderField({ + providers, + providersLoading, + selectedProvider, + selectedProviderId, + onProviderChange, +}: { + providers: AcpProvider[]; + providersLoading: boolean; + selectedProvider: AcpProvider | null; + selectedProviderId: string; + onProviderChange: (value: string) => void; +}) { + return ( +
+ + + {selectedProvider ? ( +

+ Detected via{" "} + + {describeResolvedCommand( + selectedProvider.command, + selectedProvider.binaryPath, + )} + +

+ ) : providersLoading ? ( +

+ Looking for installed ACP runtimes... +

+ ) : ( +

+ No known ACP runtime was detected. You can still enter a custom + command in Advanced setup. +

+ )}
); } @@ -61,37 +106,45 @@ export function CreateAgentRuntimeFields({ agentArgs, agentCommand, mcpCommand, - providers, - providersLoading, - selectedProvider, + relayUrl, selectedProviderId, turnTimeoutSeconds, onAcpCommandChange, onAgentArgsChange, onAgentCommandChange, onMcpCommandChange, - onProviderChange, + onRelayUrlChange, onTurnTimeoutChange, }: { acpCommand: string; agentArgs: string; agentCommand: string; mcpCommand: string; - providers: AcpProvider[]; - providersLoading: boolean; - selectedProvider: AcpProvider | null; + relayUrl: string; selectedProviderId: string; turnTimeoutSeconds: string; onAcpCommandChange: (value: string) => void; onAgentArgsChange: (value: string) => void; onAgentCommandChange: (value: string) => void; onMcpCommandChange: (value: string) => void; - onProviderChange: (value: string) => void; + onRelayUrlChange: (value: string) => void; onTurnTimeoutChange: (value: string) => void; }) { return ( <>
+
+ + onRelayUrlChange(event.target.value)} + placeholder="Leave blank to use the desktop relay" + value={relayUrl} + /> +
+
- -
- - - {selectedProvider ? ( -

- Detected via{" "} - - {describeResolvedCommand( - selectedProvider.command, - selectedProvider.binaryPath, - )} - -

- ) : providersLoading ? ( -

- Looking for installed ACP runtimes... -

- ) : ( -

- No known ACP runtime was detected. You can still enter a custom - command below. -

- )} -
{selectedProviderId === "custom" ? ( @@ -365,17 +379,17 @@ export function CreateAgentTokenSection({
-

Token scopes

-
- {AGENT_SCOPE_OPTIONS.map((scope) => { +

Scopes

+
+ {MANAGED_AGENT_SCOPE_OPTIONS.map((scope) => { const selected = selectedScopes.has(scope.value); return (
) : null} + {created.profileSyncError ? ( +

+ {created.profileSyncError} +

+ ) : null} + {created.spawnError ? (

{created.spawnError} diff --git a/desktop/src/features/agents/ui/agentUi.ts b/desktop/src/features/agents/ui/agentUi.ts index f24850cb36..923a80825b 100644 --- a/desktop/src/features/agents/ui/agentUi.ts +++ b/desktop/src/features/agents/ui/agentUi.ts @@ -1,17 +1,3 @@ -import type { TokenScope } from "@/shared/api/types"; - -export const AGENT_SCOPE_OPTIONS: Array<{ value: TokenScope; label: string }> = - [ - { value: "messages:read", label: "Messages read" }, - { value: "messages:write", label: "Messages write" }, - { value: "channels:read", label: "Channels read" }, - { value: "channels:write", label: "Channels write" }, - { value: "users:read", label: "Users read" }, - { value: "users:write", label: "Users write" }, - { value: "files:read", label: "Files read" }, - { value: "files:write", label: "Files write" }, - ]; - export function formatTimestamp(value: string | null) { if (!value) { return "Never"; diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx index d316a96456..04084030c6 100644 --- a/desktop/src/features/sidebar/ui/AppSidebar.tsx +++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx @@ -226,12 +226,15 @@ function StreamsSection({ > onChangeName(event.target.value)} placeholder="release-notes" ref={createInputRef} + spellCheck={false} value={draftName} /> = [ + { value: "messages:read", label: "Messages: Read" }, + { value: "messages:write", label: "Messages: Write" }, + { value: "channels:read", label: "Channels: Read" }, + { value: "channels:write", label: "Channels: Write" }, + { value: "users:read", label: "Users: Read" }, + { value: "files:read", label: "Files: Read" }, + { value: "files:write", label: "Files: Write" }, +]; + +export const MANAGED_AGENT_SCOPE_OPTIONS: Array<{ + value: TokenScope; + label: string; +}> = [ + ...TOKEN_SCOPE_OPTIONS.slice(0, 5), + { value: "users:write", label: "Users: Write" }, + ...TOKEN_SCOPE_OPTIONS.slice(5), +]; + +export const DEFAULT_MANAGED_AGENT_SCOPES: TokenScope[] = [ + "messages:read", + "messages:write", + "channels:read", + "users:read", + "users:write", +]; diff --git a/desktop/src/features/tokens/ui/TokenSettingsCard.tsx b/desktop/src/features/tokens/ui/TokenSettingsCard.tsx index 538d63de23..a20aa5643c 100644 --- a/desktop/src/features/tokens/ui/TokenSettingsCard.tsx +++ b/desktop/src/features/tokens/ui/TokenSettingsCard.tsx @@ -16,6 +16,7 @@ import { useRevokeTokenMutation, useTokensQuery, } from "@/features/tokens/hooks"; +import { TOKEN_SCOPE_OPTIONS } from "@/features/tokens/lib/scopeOptions"; import { getChannelMembers } from "@/shared/api/tauri"; import type { Channel, Token, TokenScope } from "@/shared/api/types"; import { cn } from "@/shared/lib/cn"; @@ -29,16 +30,6 @@ import { } from "@/shared/ui/dialog"; import { Input } from "@/shared/ui/input"; -const ALL_SCOPES: { value: TokenScope; label: string }[] = [ - { value: "messages:read", label: "Messages: Read" }, - { value: "messages:write", label: "Messages: Write" }, - { value: "channels:read", label: "Channels: Read" }, - { value: "channels:write", label: "Channels: Write" }, - { value: "users:read", label: "Users: Read" }, - { value: "files:read", label: "Files: Read" }, - { value: "files:write", label: "Files: Write" }, -]; - const EXPIRY_OPTIONS = [ { value: 7, label: "7 days" }, { value: 30, label: "30 days" }, @@ -375,7 +366,7 @@ function CreateTokenDialog({

Scopes

- {ALL_SCOPES.map(({ value, label }) => { + {TOKEN_SCOPE_OPTIONS.map(({ value, label }) => { const isSelected = selectedScopes.has(value); return (