generated from nhcarrigan/template
1712 lines
68 KiB
Svelte
1712 lines
68 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
configStore,
|
|
type HikariConfig,
|
|
type Theme,
|
|
type CustomThemeColors,
|
|
applyFontSize,
|
|
applyCustomFont,
|
|
applyCustomUiFont,
|
|
applyCustomThemeColors,
|
|
MIN_FONT_SIZE,
|
|
MAX_FONT_SIZE,
|
|
DEFAULT_FONT_SIZE,
|
|
} from "$lib/stores/config";
|
|
import { claudeStore } from "$lib/stores/claude";
|
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { open } from "@tauri-apps/plugin-dialog";
|
|
import CostSummary from "./CostSummary.svelte";
|
|
|
|
let config: HikariConfig = $state({
|
|
model: null,
|
|
api_key: null,
|
|
custom_instructions: null,
|
|
mcp_servers_json: null,
|
|
auto_granted_tools: [],
|
|
theme: "dark",
|
|
greeting_enabled: true,
|
|
greeting_custom_prompt: null,
|
|
notifications_enabled: true,
|
|
notification_volume: 0.7,
|
|
always_on_top: false,
|
|
update_checks_enabled: true,
|
|
character_panel_width: null,
|
|
font_size: 14,
|
|
streamer_mode: false,
|
|
streamer_hide_paths: false,
|
|
compact_mode: false,
|
|
profile_name: null,
|
|
profile_avatar_path: null,
|
|
profile_bio: null,
|
|
custom_theme_colors: {
|
|
bg_primary: null,
|
|
bg_secondary: null,
|
|
bg_terminal: null,
|
|
accent_primary: null,
|
|
accent_secondary: null,
|
|
text_primary: null,
|
|
text_secondary: null,
|
|
border_color: null,
|
|
},
|
|
budget_enabled: false,
|
|
session_token_budget: null,
|
|
session_cost_budget: null,
|
|
budget_action: "warn",
|
|
budget_warning_threshold: 0.8,
|
|
discord_rpc_enabled: true,
|
|
show_thinking_blocks: true,
|
|
use_worktree: false,
|
|
disable_1m_context: false,
|
|
disable_cron: false,
|
|
include_git_instructions: true,
|
|
enable_claudeai_mcp_servers: true,
|
|
auto_memory_directory: null,
|
|
model_overrides: null,
|
|
disable_skill_shell_execution: false,
|
|
max_output_tokens: null,
|
|
trusted_workspaces: [],
|
|
background_image_path: null,
|
|
background_image_opacity: 0.3,
|
|
custom_font_path: null,
|
|
custom_font_family: null,
|
|
custom_ui_font_path: null,
|
|
custom_ui_font_family: null,
|
|
task_loop_auto_commit: false,
|
|
task_loop_commit_prefix: "feat",
|
|
task_loop_include_summary: false,
|
|
});
|
|
|
|
let showCustomThemeEditor = $state(false);
|
|
let customFontPathInput = $state("");
|
|
let customFontFamilyInput = $state("");
|
|
let customFontStatus: string | null = $state(null);
|
|
let customUiFontPathInput = $state("");
|
|
let customUiFontFamilyInput = $state("");
|
|
let customUiFontStatus: string | null = $state(null);
|
|
let modelOverridesJson = $state("");
|
|
let modelOverridesError: string | null = $state(null);
|
|
|
|
interface AuthStatus {
|
|
is_logged_in: boolean;
|
|
email: string | null;
|
|
org_id: string | null;
|
|
org_name: string | null;
|
|
api_key_source: string | null;
|
|
api_provider: string | null;
|
|
subscription_type: string | null;
|
|
}
|
|
|
|
let authStatus: AuthStatus | null = $state(null);
|
|
let authLoading = $state(false);
|
|
let authActionLoading = $state(false);
|
|
let authError: string | null = $state(null);
|
|
|
|
let isOpen = $state(false);
|
|
let isSaving = $state(false);
|
|
let saveError: string | null = $state(null);
|
|
let newToolName = $state("");
|
|
let showApiKey = $state(false);
|
|
let grantedTools: string[] = $state([]);
|
|
|
|
configStore.config.subscribe((c) => {
|
|
config = { ...c };
|
|
customFontPathInput = c.custom_font_path ?? "";
|
|
customFontFamilyInput = c.custom_font_family ?? "";
|
|
customUiFontPathInput = c.custom_ui_font_path ?? "";
|
|
customUiFontFamilyInput = c.custom_ui_font_family ?? "";
|
|
modelOverridesJson = c.model_overrides ? JSON.stringify(c.model_overrides, null, 2) : "";
|
|
});
|
|
|
|
configStore.isSidebarOpen.subscribe((open) => {
|
|
isOpen = open;
|
|
if (open && authStatus === null) {
|
|
void refreshAuthStatus();
|
|
}
|
|
});
|
|
|
|
configStore.saveError.subscribe((error) => {
|
|
saveError = error;
|
|
});
|
|
|
|
claudeStore.grantedTools.subscribe((tools) => {
|
|
grantedTools = Array.from(tools);
|
|
});
|
|
|
|
const availableModels = [
|
|
{ value: "", label: "Default (from ~/.claude)" },
|
|
// Current generation (Claude 4.6)
|
|
{ value: "claude-opus-4-6", label: "Claude Opus 4.6 (Most Capable)" },
|
|
{ value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 (Recommended)" },
|
|
// Previous generation (Claude 4.5)
|
|
{ value: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" },
|
|
{ value: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5 (Fast & Cheap)" },
|
|
{ value: "claude-opus-4-5-20251101", label: "Claude Opus 4.5" },
|
|
// Previous generation (Claude 4.x)
|
|
{ value: "claude-opus-4-1-20250805", label: "Claude Opus 4.1" },
|
|
{ value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4" },
|
|
{ value: "claude-opus-4-20250514", label: "Claude Opus 4" },
|
|
];
|
|
|
|
const commonTools = [
|
|
"Read",
|
|
"Write",
|
|
"Edit",
|
|
"Bash",
|
|
"Glob",
|
|
"Grep",
|
|
"WebFetch",
|
|
"WebSearch",
|
|
"Task",
|
|
];
|
|
|
|
async function refreshAuthStatus() {
|
|
authLoading = true;
|
|
authError = null;
|
|
try {
|
|
authStatus = await invoke<AuthStatus>("get_auth_status");
|
|
} catch (e) {
|
|
authError = String(e);
|
|
} finally {
|
|
authLoading = false;
|
|
}
|
|
}
|
|
|
|
async function handleAuthLogin() {
|
|
authActionLoading = true;
|
|
authError = null;
|
|
try {
|
|
await invoke<string>("auth_login");
|
|
await refreshAuthStatus();
|
|
} catch (e) {
|
|
authError = String(e);
|
|
} finally {
|
|
authActionLoading = false;
|
|
}
|
|
}
|
|
|
|
async function handleAuthLogout() {
|
|
authActionLoading = true;
|
|
authError = null;
|
|
try {
|
|
await invoke<string>("auth_logout");
|
|
await refreshAuthStatus();
|
|
} catch (e) {
|
|
authError = String(e);
|
|
} finally {
|
|
authActionLoading = false;
|
|
}
|
|
}
|
|
|
|
async function handleSave() {
|
|
isSaving = true;
|
|
saveError = null;
|
|
modelOverridesError = null;
|
|
try {
|
|
if (modelOverridesJson.trim()) {
|
|
config.model_overrides = JSON.parse(modelOverridesJson) as Record<string, string>;
|
|
} else {
|
|
config.model_overrides = null;
|
|
}
|
|
} catch {
|
|
modelOverridesError = "Invalid JSON — please check your model overrides.";
|
|
isSaving = false;
|
|
return;
|
|
}
|
|
try {
|
|
await configStore.saveConfig(config);
|
|
configStore.closeSidebar();
|
|
} catch {
|
|
// Error is handled by the store
|
|
} finally {
|
|
isSaving = false;
|
|
}
|
|
}
|
|
|
|
async function handleThemeChange(theme: Theme) {
|
|
config.theme = theme;
|
|
showCustomThemeEditor = theme === "custom";
|
|
await configStore.setTheme(theme, config.custom_theme_colors);
|
|
}
|
|
|
|
function handleCustomColorChange(key: keyof CustomThemeColors, value: string) {
|
|
config.custom_theme_colors = {
|
|
...config.custom_theme_colors,
|
|
[key]: value || null,
|
|
};
|
|
// Live preview
|
|
if (config.theme === "custom") {
|
|
applyCustomThemeColors(config.custom_theme_colors);
|
|
}
|
|
}
|
|
|
|
// Default dark theme colors for color picker defaults
|
|
const defaultDarkColors: Required<Record<keyof CustomThemeColors, string>> = {
|
|
bg_primary: "#1a1a2e",
|
|
bg_secondary: "#16213e",
|
|
bg_terminal: "#0f0f1a",
|
|
accent_primary: "#e94560",
|
|
accent_secondary: "#ff6b9d",
|
|
text_primary: "#ffffff",
|
|
text_secondary: "#a0a0a0",
|
|
border_color: "#2a2a4a",
|
|
};
|
|
|
|
function toggleTool(tool: string) {
|
|
if (config.auto_granted_tools.includes(tool)) {
|
|
config.auto_granted_tools = config.auto_granted_tools.filter((t) => t !== tool);
|
|
} else {
|
|
config.auto_granted_tools = [...config.auto_granted_tools, tool];
|
|
}
|
|
}
|
|
|
|
function addCustomTool() {
|
|
if (newToolName.trim() && !config.auto_granted_tools.includes(newToolName.trim())) {
|
|
config.auto_granted_tools = [...config.auto_granted_tools, newToolName.trim()];
|
|
newToolName = "";
|
|
}
|
|
}
|
|
|
|
function removeTool(tool: string) {
|
|
config.auto_granted_tools = config.auto_granted_tools.filter((t) => t !== tool);
|
|
}
|
|
|
|
function importFromSession() {
|
|
config.auto_granted_tools = [...new Set([...config.auto_granted_tools, ...grantedTools])];
|
|
}
|
|
|
|
async function handleAlwaysOnTopChange(enabled: boolean) {
|
|
config.always_on_top = enabled;
|
|
const window = getCurrentWindow();
|
|
await window.setAlwaysOnTop(enabled);
|
|
await configStore.updateConfig({ always_on_top: enabled });
|
|
}
|
|
|
|
async function pickBackgroundImage() {
|
|
const selected = await open({
|
|
multiple: false,
|
|
filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "webp", "gif", "avif"] }],
|
|
});
|
|
if (selected) {
|
|
config.background_image_path = selected;
|
|
}
|
|
}
|
|
|
|
function clearBackgroundImage() {
|
|
config.background_image_path = null;
|
|
}
|
|
</script>
|
|
|
|
<!-- Backdrop -->
|
|
{#if isOpen}
|
|
<div
|
|
class="fixed inset-0 bg-black/50 z-40 transition-opacity"
|
|
onclick={configStore.closeSidebar}
|
|
onkeydown={(e) => e.key === "Escape" && configStore.closeSidebar()}
|
|
role="button"
|
|
tabindex="-1"
|
|
aria-label="Close sidebar"
|
|
></div>
|
|
{/if}
|
|
|
|
<!-- Sidebar -->
|
|
<aside
|
|
class="fixed right-0 top-0 h-full w-96 bg-[var(--bg-secondary)] border-l border-[var(--border-color)] z-50 transform transition-transform duration-300 ease-in-out overflow-y-auto {isOpen
|
|
? 'translate-x-0'
|
|
: 'translate-x-full'}"
|
|
>
|
|
<div class="p-4">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-lg font-semibold text-[var(--text-primary)]">Settings</h2>
|
|
<button
|
|
onclick={configStore.closeSidebar}
|
|
class="p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
|
aria-label="Close settings"
|
|
>
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{#if saveError}
|
|
<div class="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400 text-sm">
|
|
{saveError}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Account Section -->
|
|
<section class="mb-6">
|
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
|
Account
|
|
</h3>
|
|
|
|
{#if authLoading}
|
|
<div class="text-sm text-[var(--text-secondary)] py-2">Checking auth status...</div>
|
|
{:else if authStatus}
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<span
|
|
class="inline-block w-2.5 h-2.5 rounded-full flex-shrink-0 {authStatus.is_logged_in
|
|
? 'bg-green-500'
|
|
: 'bg-red-500'}"
|
|
></span>
|
|
<span class="text-sm font-medium text-[var(--text-primary)]">
|
|
{authStatus.is_logged_in ? "Logged in" : "Not logged in"}
|
|
</span>
|
|
</div>
|
|
{#if authStatus.email || authStatus.org_name || authStatus.api_key_source || config.api_key}
|
|
<dl class="text-xs space-y-1 mb-3">
|
|
{#if authStatus.email}
|
|
<div class="flex gap-2">
|
|
<dt class="text-[var(--text-tertiary)] w-20 flex-shrink-0">Email</dt>
|
|
<dd class="text-[var(--text-primary)] break-all">{authStatus.email}</dd>
|
|
</div>
|
|
{/if}
|
|
{#if authStatus.org_name}
|
|
<div class="flex gap-2">
|
|
<dt class="text-[var(--text-tertiary)] w-20 flex-shrink-0">Org</dt>
|
|
<dd class="text-[var(--text-primary)]">{authStatus.org_name}</dd>
|
|
</div>
|
|
{/if}
|
|
{#if authStatus.org_id}
|
|
<div class="flex gap-2">
|
|
<dt class="text-[var(--text-tertiary)] w-20 flex-shrink-0">Org UUID</dt>
|
|
<dd class="text-[var(--text-secondary)] font-mono text-[10px] break-all">
|
|
{authStatus.org_id}
|
|
</dd>
|
|
</div>
|
|
{/if}
|
|
{#if authStatus.api_key_source}
|
|
<div class="flex gap-2">
|
|
<dt class="text-[var(--text-tertiary)] w-20 flex-shrink-0">API key</dt>
|
|
<dd class="text-[var(--text-primary)]">{authStatus.api_key_source}</dd>
|
|
</div>
|
|
{/if}
|
|
{#if authStatus.subscription_type}
|
|
<div class="flex gap-2">
|
|
<dt class="text-[var(--text-tertiary)] w-20 flex-shrink-0">Plan</dt>
|
|
<dd class="text-[var(--text-primary)]">{authStatus.subscription_type}</dd>
|
|
</div>
|
|
{/if}
|
|
<div class="flex gap-2">
|
|
<dt class="text-[var(--text-tertiary)] w-20 flex-shrink-0">Override</dt>
|
|
<dd class="text-[var(--text-primary)]">
|
|
{#if config.api_key}
|
|
{config.streamer_mode ? "Custom key set 🔒" : "Custom key set"}
|
|
{:else}
|
|
None
|
|
{/if}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
{/if}
|
|
{:else}
|
|
<div class="text-sm text-[var(--text-secondary)] py-2">Auth status unavailable</div>
|
|
{/if}
|
|
|
|
{#if authError}
|
|
<div class="mb-3 p-2 bg-red-500/20 border border-red-500/50 rounded text-red-400 text-xs">
|
|
{authError}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="flex gap-2">
|
|
<button
|
|
onclick={refreshAuthStatus}
|
|
disabled={authLoading || authActionLoading}
|
|
class="px-3 py-1.5 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-secondary)] hover:border-[var(--accent-primary)] hover:text-[var(--text-primary)] transition-colors disabled:opacity-50"
|
|
>
|
|
Refresh
|
|
</button>
|
|
{#if authStatus && !authStatus.is_logged_in}
|
|
<button
|
|
onclick={handleAuthLogin}
|
|
disabled={authActionLoading}
|
|
class="btn-trans-gradient px-3 py-1.5 text-sm rounded-lg disabled:opacity-50"
|
|
>
|
|
{authActionLoading ? "Logging in..." : "Login"}
|
|
</button>
|
|
{:else if authStatus && authStatus.is_logged_in}
|
|
<button
|
|
onclick={handleAuthLogout}
|
|
disabled={authActionLoading}
|
|
class="px-3 py-1.5 text-sm bg-red-500/20 border border-red-500/50 rounded-lg text-red-400 hover:bg-red-500/30 transition-colors disabled:opacity-50"
|
|
>
|
|
{authActionLoading ? "Logging out..." : "Logout"}
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Agent Settings Section -->
|
|
<section class="mb-6">
|
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
|
Agent Settings
|
|
</h3>
|
|
|
|
<!-- Model Selection -->
|
|
<div class="mb-4">
|
|
<label for="model" class="block text-sm text-[var(--text-secondary)] mb-1">Model</label>
|
|
<select
|
|
id="model"
|
|
bind:value={config.model}
|
|
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent-primary)]"
|
|
>
|
|
{#each availableModels as model (model.value)}
|
|
<option value={model.value}>{model.label}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
|
|
<!-- API Key -->
|
|
<div class="mb-4">
|
|
<label for="api-key" class="block text-sm text-[var(--text-secondary)] mb-1">
|
|
API Key <span class="text-[var(--text-tertiary)]">(optional override)</span>
|
|
{#if config.streamer_mode}
|
|
<span class="text-yellow-500 text-xs ml-2">🔒 Hidden (Streamer Mode)</span>
|
|
{/if}
|
|
</label>
|
|
<div class="relative">
|
|
{#if config.streamer_mode}
|
|
<input
|
|
id="api-key"
|
|
type="password"
|
|
value="••••••••••••••••••••••••"
|
|
disabled
|
|
class="w-full px-3 py-2 pr-10 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-tertiary)] focus:outline-none cursor-not-allowed"
|
|
/>
|
|
{:else}
|
|
<input
|
|
id="api-key"
|
|
type={showApiKey ? "text" : "password"}
|
|
bind:value={config.api_key}
|
|
placeholder="Falls back to ~/.claude settings"
|
|
class="w-full px-3 py-2 pr-10 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent-primary)]"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onclick={() => (showApiKey = !showApiKey)}
|
|
class="absolute right-2 top-1/2 -translate-y-1/2 text-[var(--text-tertiary)] hover:text-[var(--text-primary)]"
|
|
aria-label={showApiKey ? "Hide API key" : "Show API key"}
|
|
>
|
|
{#if showApiKey}
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
|
/>
|
|
</svg>
|
|
{:else}
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
/>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
|
/>
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Instructions -->
|
|
<div class="mb-4">
|
|
<label for="instructions" class="block text-sm text-[var(--text-secondary)] mb-1"
|
|
>Custom Instructions</label
|
|
>
|
|
<textarea
|
|
id="instructions"
|
|
bind:value={config.custom_instructions}
|
|
rows="4"
|
|
placeholder="Additional instructions for the agent..."
|
|
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent-primary)] resize-none"
|
|
></textarea>
|
|
</div>
|
|
|
|
<!-- Worktree Isolation -->
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={config.use_worktree}
|
|
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Worktree isolation</span>
|
|
</label>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
Launch sessions with <code class="font-mono">--worktree</code> for isolated git worktree environments
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Disable 1M Context Window -->
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={config.disable_1m_context}
|
|
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Disable 1M context window</span>
|
|
</label>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
Sets <code class="font-mono">CLAUDE_CODE_DISABLE_1M_CONTEXT=1</code> to opt out of the extended
|
|
context window
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Disable Cron Scheduling -->
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={config.disable_cron}
|
|
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Disable cron scheduling</span>
|
|
</label>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
Sets <code class="font-mono">CLAUDE_CODE_DISABLE_CRON=1</code> to prevent Claude from scheduling
|
|
recurring tasks
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Disable Skill Shell Execution -->
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={config.disable_skill_shell_execution}
|
|
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Disable skill shell execution</span>
|
|
</label>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
Passes <code class="font-mono">disableSkillShellExecution: true</code> to prevent skill scripts
|
|
from executing shell commands (requires Claude Code v2.1.91+)
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Include Git Instructions -->
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={config.include_git_instructions}
|
|
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Include git instructions</span>
|
|
</label>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
When disabled, sets <code class="font-mono">CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS=1</code> to
|
|
remove Claude's built-in commit and PR workflow guidance from its system prompt
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Max Output Tokens -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm text-[var(--text-primary)] mb-1" for="max-output-tokens">
|
|
Max output tokens
|
|
</label>
|
|
<input
|
|
id="max-output-tokens"
|
|
type="number"
|
|
min="1"
|
|
max="128000"
|
|
placeholder="Default (model-dependent)"
|
|
bind:value={config.max_output_tokens}
|
|
class="w-full px-3 py-2 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-[var(--text-primary)] placeholder-[var(--text-tertiary)] focus:outline-none focus:border-[var(--accent-primary)]"
|
|
/>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1">
|
|
Sets <code class="font-mono">CLAUDE_CODE_MAX_OUTPUT_TOKENS</code>. Maximum: 128k tokens
|
|
for Opus 4.6 and Sonnet 4.6 (64k default for Opus 4.6, 32k for other models). Increase if
|
|
responses are being cut off.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Auto-memory Directory -->
|
|
<div class="mb-4">
|
|
<label for="auto-memory-dir" class="block text-sm text-[var(--text-primary)] mb-1">
|
|
Auto-memory directory <span class="text-[var(--text-tertiary)]">(optional)</span>
|
|
</label>
|
|
<input
|
|
id="auto-memory-dir"
|
|
type="text"
|
|
placeholder="Leave blank to use default"
|
|
bind:value={config.auto_memory_directory}
|
|
class="w-full px-3 py-2 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-[var(--text-primary)] placeholder-[var(--text-tertiary)] focus:outline-none focus:border-[var(--accent-primary)]"
|
|
/>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1">
|
|
Custom directory for auto-memory storage. Passed via
|
|
<code class="font-mono">--settings autoMemoryDirectory</code>. Leave blank to use the
|
|
default (working directory).
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Model Overrides -->
|
|
<div class="mb-4">
|
|
<label for="model-overrides" class="block text-sm text-[var(--text-primary)] mb-1">
|
|
Model overrides <span class="text-[var(--text-tertiary)]">(optional)</span>
|
|
</label>
|
|
<textarea
|
|
id="model-overrides"
|
|
rows={4}
|
|
placeholder={'{\n "claude-opus-4-6": "arn:aws:bedrock:..."\n}'}
|
|
bind:value={modelOverridesJson}
|
|
class="w-full px-3 py-2 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-[var(--text-primary)] placeholder-[var(--text-tertiary)] focus:outline-none focus:border-[var(--accent-primary)] font-mono resize-y"
|
|
></textarea>
|
|
{#if modelOverridesError}
|
|
<p class="text-xs text-red-500 mt-1">{modelOverridesError}</p>
|
|
{/if}
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1">
|
|
JSON map of model names to provider-specific IDs (for AWS Bedrock, Google Vertex, etc.).
|
|
Passed via <code class="font-mono">--settings modelOverrides</code>. Leave blank to use
|
|
defaults.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Greeting Section -->
|
|
<section class="mb-6">
|
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
|
Greeting
|
|
</h3>
|
|
|
|
<!-- Enable/Disable Toggle -->
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={config.greeting_enabled}
|
|
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Send greeting on connect</span>
|
|
</label>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
Automatically greet you when a session starts with time-based messages
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Custom Greeting Prompt -->
|
|
{#if config.greeting_enabled}
|
|
<div class="mb-4">
|
|
<label for="greeting-prompt" class="block text-sm text-[var(--text-secondary)] mb-1">
|
|
Custom Greeting Prompt <span class="text-[var(--text-tertiary)]">(optional)</span>
|
|
</label>
|
|
<textarea
|
|
id="greeting-prompt"
|
|
bind:value={config.greeting_custom_prompt}
|
|
rows="3"
|
|
placeholder="Leave empty for time-based greetings, or customize how you'd like to be greeted..."
|
|
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent-primary)] resize-none"
|
|
></textarea>
|
|
</div>
|
|
{/if}
|
|
</section>
|
|
|
|
<!-- MCP Servers Section -->
|
|
<section class="mb-6">
|
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
|
MCP Servers
|
|
</h3>
|
|
<div class="mb-2">
|
|
<label for="mcp-config" class="block text-sm text-[var(--text-secondary)] mb-1">
|
|
Server Configuration <span class="text-[var(--text-tertiary)]">(JSON)</span>
|
|
</label>
|
|
<textarea
|
|
id="mcp-config"
|
|
bind:value={config.mcp_servers_json}
|
|
rows="6"
|
|
placeholder={`{\n "servers": {\n "example": {\n "command": "npx",\n "args": ["-y", "@example/mcp-server"]\n }\n }\n}`}
|
|
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] font-mono text-sm focus:outline-none focus:border-[var(--accent-primary)] resize-none"
|
|
></textarea>
|
|
</div>
|
|
|
|
<!-- Enable Claude.ai MCP Servers -->
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={config.enable_claudeai_mcp_servers}
|
|
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Enable Claude.ai MCP servers</span>
|
|
</label>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
When disabled, sets <code class="font-mono">ENABLE_CLAUDEAI_MCP_SERVERS=false</code> to prevent
|
|
Claude Code from connecting to MCP servers configured in Claude.ai.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Auto-Granted Tools Section -->
|
|
<section class="mb-6">
|
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
|
Auto-Granted Tools
|
|
</h3>
|
|
<p class="text-xs text-[var(--text-tertiary)] mb-3">
|
|
These tools will be pre-approved for every session (no permission prompts).
|
|
</p>
|
|
|
|
<!-- Common tools checkboxes -->
|
|
<div class="grid grid-cols-2 gap-2 mb-3">
|
|
{#each commonTools as tool (tool)}
|
|
<label class="flex items-center gap-2 text-sm text-[var(--text-primary)] cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={config.auto_granted_tools.includes(tool)}
|
|
onchange={() => toggleTool(tool)}
|
|
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
|
|
/>
|
|
{tool}
|
|
</label>
|
|
{/each}
|
|
</div>
|
|
|
|
<!-- Currently granted tools (with import) -->
|
|
{#if grantedTools.length > 0}
|
|
<div class="mb-3">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="text-xs text-[var(--text-tertiary)]">Session-granted tools:</span>
|
|
<button
|
|
onclick={importFromSession}
|
|
class="text-xs text-[var(--accent-primary)] hover:text-[var(--accent-secondary)] transition-colors"
|
|
>
|
|
Import all
|
|
</button>
|
|
</div>
|
|
<div class="flex flex-wrap gap-1">
|
|
{#each grantedTools as tool (tool)}
|
|
<span
|
|
class="px-2 py-0.5 text-xs bg-[var(--accent-primary)]/20 text-[var(--accent-primary)] rounded"
|
|
>
|
|
{tool}
|
|
</span>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Custom tools list -->
|
|
{#if config.auto_granted_tools.filter((t) => !commonTools.includes(t)).length > 0}
|
|
<div class="mb-3">
|
|
<span class="text-xs text-[var(--text-tertiary)] block mb-2">Custom tools:</span>
|
|
<div class="flex flex-wrap gap-1">
|
|
{#each config.auto_granted_tools.filter((t) => !commonTools.includes(t)) as tool (tool)}
|
|
<span
|
|
class="px-2 py-0.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded flex items-center gap-1"
|
|
>
|
|
{tool}
|
|
<button
|
|
onclick={() => removeTool(tool)}
|
|
class="text-[var(--text-tertiary)] hover:text-red-400"
|
|
aria-label="Remove {tool}"
|
|
>
|
|
×
|
|
</button>
|
|
</span>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Add custom tool -->
|
|
<div class="flex gap-2">
|
|
<input
|
|
type="text"
|
|
bind:value={newToolName}
|
|
placeholder="Add custom tool..."
|
|
onkeydown={(e) => e.key === "Enter" && addCustomTool()}
|
|
class="flex-1 px-3 py-1.5 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent-primary)]"
|
|
/>
|
|
<button
|
|
onclick={addCustomTool}
|
|
disabled={!newToolName.trim()}
|
|
class="btn-trans-gradient px-3 py-1.5 text-sm rounded-lg"
|
|
>
|
|
Add
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Appearance Section -->
|
|
<section class="mb-6">
|
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
|
Appearance
|
|
</h3>
|
|
|
|
<!-- Theme Selection -->
|
|
<div class="mb-4">
|
|
<span class="block text-sm text-[var(--text-secondary)] mb-2">Theme</span>
|
|
<div class="flex flex-wrap gap-2" role="group" aria-label="Theme selection">
|
|
<button
|
|
onclick={() => handleThemeChange("dark")}
|
|
class="flex-1 min-w-[70px] px-3 py-2 text-sm rounded-lg border transition-colors {config.theme ===
|
|
'dark'
|
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
|
>
|
|
Dark
|
|
</button>
|
|
<button
|
|
onclick={() => handleThemeChange("light")}
|
|
class="flex-1 min-w-[70px] px-3 py-2 text-sm rounded-lg border transition-colors {config.theme ===
|
|
'light'
|
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
|
>
|
|
Light
|
|
</button>
|
|
<button
|
|
onclick={() => handleThemeChange("high-contrast")}
|
|
class="flex-1 min-w-[70px] px-3 py-2 text-sm rounded-lg border transition-colors {config.theme ===
|
|
'high-contrast'
|
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
|
title="High contrast mode for improved accessibility"
|
|
>
|
|
Contrast
|
|
</button>
|
|
<button
|
|
onclick={() => handleThemeChange("custom")}
|
|
class="flex-1 min-w-[70px] px-3 py-2 text-sm rounded-lg border transition-colors {config.theme ===
|
|
'custom'
|
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
|
title="Create your own custom theme"
|
|
>
|
|
Custom
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Preset Themes — Dark -->
|
|
<span class="block text-xs text-[var(--text-tertiary)] mt-3 mb-2">Dark Presets</span>
|
|
<div class="flex flex-wrap gap-2" role="group" aria-label="Dark preset theme selection">
|
|
<button
|
|
onclick={() => handleThemeChange("dracula")}
|
|
class="flex-1 min-w-[70px] px-3 py-2 text-sm rounded-lg border transition-colors {config.theme ===
|
|
'dracula'
|
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
|
title="Dracula theme"
|
|
>
|
|
Dracula
|
|
</button>
|
|
<button
|
|
onclick={() => handleThemeChange("catppuccin")}
|
|
class="flex-1 min-w-[70px] px-3 py-2 text-sm rounded-lg border transition-colors {config.theme ===
|
|
'catppuccin'
|
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
|
title="Catppuccin Mocha theme"
|
|
>
|
|
Catppuccin
|
|
</button>
|
|
<button
|
|
onclick={() => handleThemeChange("nord")}
|
|
class="flex-1 min-w-[70px] px-3 py-2 text-sm rounded-lg border transition-colors {config.theme ===
|
|
'nord'
|
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
|
title="Nord theme"
|
|
>
|
|
Nord
|
|
</button>
|
|
<button
|
|
onclick={() => handleThemeChange("solarized")}
|
|
class="flex-1 min-w-[70px] px-3 py-2 text-sm rounded-lg border transition-colors {config.theme ===
|
|
'solarized'
|
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
|
title="Solarized Dark theme"
|
|
>
|
|
Solarized
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Preset Themes — Light -->
|
|
<span class="block text-xs text-[var(--text-tertiary)] mt-3 mb-2">Light Presets</span>
|
|
<div class="flex flex-wrap gap-2" role="group" aria-label="Light preset theme selection">
|
|
<button
|
|
onclick={() => handleThemeChange("solarized-light")}
|
|
class="flex-1 min-w-[70px] px-3 py-2 text-sm rounded-lg border transition-colors {config.theme ===
|
|
'solarized-light'
|
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
|
title="Solarized Light theme"
|
|
>
|
|
Solarized
|
|
</button>
|
|
<button
|
|
onclick={() => handleThemeChange("catppuccin-latte")}
|
|
class="flex-1 min-w-[70px] px-3 py-2 text-sm rounded-lg border transition-colors {config.theme ===
|
|
'catppuccin-latte'
|
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
|
title="Catppuccin Latte theme"
|
|
>
|
|
Latte
|
|
</button>
|
|
<button
|
|
onclick={() => handleThemeChange("gruvbox-light")}
|
|
class="flex-1 min-w-[70px] px-3 py-2 text-sm rounded-lg border transition-colors {config.theme ===
|
|
'gruvbox-light'
|
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
|
title="Gruvbox Light theme"
|
|
>
|
|
Gruvbox
|
|
</button>
|
|
<button
|
|
onclick={() => handleThemeChange("rose-pine-dawn")}
|
|
class="flex-1 min-w-[70px] px-3 py-2 text-sm rounded-lg border transition-colors {config.theme ===
|
|
'rose-pine-dawn'
|
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
|
title="Rosé Pine Dawn theme"
|
|
>
|
|
Rosé Pine
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Theme Editor -->
|
|
{#if config.theme === "custom" || showCustomThemeEditor}
|
|
<div class="mb-4 p-3 bg-[var(--bg-primary)] rounded-lg border border-[var(--border-color)]">
|
|
<h4 class="text-sm font-medium text-[var(--text-primary)] mb-3">Custom Theme Colors</h4>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div class="color-input-group">
|
|
<label class="text-xs text-[var(--text-secondary)]" for="color-bg-primary"
|
|
>Background</label
|
|
>
|
|
<div class="flex gap-2 items-center">
|
|
<input
|
|
id="color-bg-primary"
|
|
type="color"
|
|
value={config.custom_theme_colors.bg_primary || defaultDarkColors.bg_primary}
|
|
oninput={(e) => handleCustomColorChange("bg_primary", e.currentTarget.value)}
|
|
class="color-picker"
|
|
/>
|
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
|
{config.custom_theme_colors.bg_primary || defaultDarkColors.bg_primary}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="color-input-group">
|
|
<label class="text-xs text-[var(--text-secondary)]" for="color-bg-secondary"
|
|
>Secondary BG</label
|
|
>
|
|
<div class="flex gap-2 items-center">
|
|
<input
|
|
id="color-bg-secondary"
|
|
type="color"
|
|
value={config.custom_theme_colors.bg_secondary || defaultDarkColors.bg_secondary}
|
|
oninput={(e) => handleCustomColorChange("bg_secondary", e.currentTarget.value)}
|
|
class="color-picker"
|
|
/>
|
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
|
{config.custom_theme_colors.bg_secondary || defaultDarkColors.bg_secondary}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="color-input-group">
|
|
<label class="text-xs text-[var(--text-secondary)]" for="color-bg-terminal"
|
|
>Terminal BG</label
|
|
>
|
|
<div class="flex gap-2 items-center">
|
|
<input
|
|
id="color-bg-terminal"
|
|
type="color"
|
|
value={config.custom_theme_colors.bg_terminal || defaultDarkColors.bg_terminal}
|
|
oninput={(e) => handleCustomColorChange("bg_terminal", e.currentTarget.value)}
|
|
class="color-picker"
|
|
/>
|
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
|
{config.custom_theme_colors.bg_terminal || defaultDarkColors.bg_terminal}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="color-input-group">
|
|
<label class="text-xs text-[var(--text-secondary)]" for="color-border">Border</label>
|
|
<div class="flex gap-2 items-center">
|
|
<input
|
|
id="color-border"
|
|
type="color"
|
|
value={config.custom_theme_colors.border_color || defaultDarkColors.border_color}
|
|
oninput={(e) => handleCustomColorChange("border_color", e.currentTarget.value)}
|
|
class="color-picker"
|
|
/>
|
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
|
{config.custom_theme_colors.border_color || defaultDarkColors.border_color}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="color-input-group">
|
|
<label class="text-xs text-[var(--text-secondary)]" for="color-accent-primary"
|
|
>Accent Primary</label
|
|
>
|
|
<div class="flex gap-2 items-center">
|
|
<input
|
|
id="color-accent-primary"
|
|
type="color"
|
|
value={config.custom_theme_colors.accent_primary ||
|
|
defaultDarkColors.accent_primary}
|
|
oninput={(e) => handleCustomColorChange("accent_primary", e.currentTarget.value)}
|
|
class="color-picker"
|
|
/>
|
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
|
{config.custom_theme_colors.accent_primary || defaultDarkColors.accent_primary}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="color-input-group">
|
|
<label class="text-xs text-[var(--text-secondary)]" for="color-accent-secondary"
|
|
>Accent Secondary</label
|
|
>
|
|
<div class="flex gap-2 items-center">
|
|
<input
|
|
id="color-accent-secondary"
|
|
type="color"
|
|
value={config.custom_theme_colors.accent_secondary ||
|
|
defaultDarkColors.accent_secondary}
|
|
oninput={(e) =>
|
|
handleCustomColorChange("accent_secondary", e.currentTarget.value)}
|
|
class="color-picker"
|
|
/>
|
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
|
{config.custom_theme_colors.accent_secondary ||
|
|
defaultDarkColors.accent_secondary}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="color-input-group">
|
|
<label class="text-xs text-[var(--text-secondary)]" for="color-text-primary"
|
|
>Text Primary</label
|
|
>
|
|
<div class="flex gap-2 items-center">
|
|
<input
|
|
id="color-text-primary"
|
|
type="color"
|
|
value={config.custom_theme_colors.text_primary || defaultDarkColors.text_primary}
|
|
oninput={(e) => handleCustomColorChange("text_primary", e.currentTarget.value)}
|
|
class="color-picker"
|
|
/>
|
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
|
{config.custom_theme_colors.text_primary || defaultDarkColors.text_primary}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="color-input-group">
|
|
<label class="text-xs text-[var(--text-secondary)]" for="color-text-secondary"
|
|
>Text Secondary</label
|
|
>
|
|
<div class="flex gap-2 items-center">
|
|
<input
|
|
id="color-text-secondary"
|
|
type="color"
|
|
value={config.custom_theme_colors.text_secondary ||
|
|
defaultDarkColors.text_secondary}
|
|
oninput={(e) => handleCustomColorChange("text_secondary", e.currentTarget.value)}
|
|
class="color-picker"
|
|
/>
|
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
|
{config.custom_theme_colors.text_secondary || defaultDarkColors.text_secondary}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-3">
|
|
Changes preview live. Click Save Settings to persist.
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Font Size -->
|
|
<div class="mb-4">
|
|
<label for="font-size" class="block text-sm text-[var(--text-secondary)] mb-2">
|
|
Terminal Font Size
|
|
</label>
|
|
<div class="flex items-center gap-3">
|
|
<input
|
|
id="font-size"
|
|
type="range"
|
|
bind:value={config.font_size}
|
|
oninput={() => applyFontSize(config.font_size)}
|
|
min={MIN_FONT_SIZE}
|
|
max={MAX_FONT_SIZE}
|
|
step="1"
|
|
class="flex-1 h-2 bg-[var(--bg-primary)] rounded-lg appearance-none cursor-pointer"
|
|
/>
|
|
<span class="text-sm text-gray-300 w-12 text-right">{config.font_size}px</span>
|
|
<button
|
|
onclick={() => {
|
|
config.font_size = DEFAULT_FONT_SIZE;
|
|
applyFontSize(DEFAULT_FONT_SIZE);
|
|
}}
|
|
class="px-2 py-1 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded hover:border-[var(--accent-primary)] text-[var(--text-secondary)] transition-colors"
|
|
title="Reset to default (14px)"
|
|
>
|
|
Reset
|
|
</button>
|
|
</div>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1">
|
|
Use Ctrl++ / Ctrl+- to quickly adjust, Ctrl+0 to reset
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Custom Terminal Font -->
|
|
<div class="mb-4">
|
|
<span class="block text-sm text-[var(--text-secondary)] mb-2">Custom Terminal Font</span>
|
|
<div class="flex flex-col gap-2">
|
|
<input
|
|
type="text"
|
|
bind:value={customFontPathInput}
|
|
placeholder="URL or local file path (e.g. /path/to/font.ttf)"
|
|
class="w-full px-3 py-2 text-sm rounded-lg border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-primary)] placeholder-gray-500 focus:outline-none focus:border-[var(--accent-primary)]"
|
|
/>
|
|
<input
|
|
type="text"
|
|
bind:value={customFontFamilyInput}
|
|
placeholder="Font family name (e.g. FiraCode)"
|
|
class="w-full px-3 py-2 text-sm rounded-lg border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-primary)] placeholder-gray-500 focus:outline-none focus:border-[var(--accent-primary)]"
|
|
/>
|
|
<div class="flex gap-2">
|
|
<button
|
|
onclick={async () => {
|
|
customFontStatus = null;
|
|
try {
|
|
await configStore.setCustomFont(
|
|
customFontPathInput || null,
|
|
customFontFamilyInput || null
|
|
);
|
|
customFontStatus = "Font applied!";
|
|
} catch (e) {
|
|
customFontStatus = `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
}
|
|
}}
|
|
class="flex-1 px-3 py-2 text-sm rounded-lg border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)] transition-colors"
|
|
>
|
|
Apply
|
|
</button>
|
|
<button
|
|
onclick={async () => {
|
|
customFontStatus = null;
|
|
customFontPathInput = "";
|
|
customFontFamilyInput = "";
|
|
try {
|
|
await configStore.setCustomFont(null, null);
|
|
await applyCustomFont(null, null);
|
|
customFontStatus = "Font reset to default.";
|
|
} catch (e) {
|
|
customFontStatus = `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
}
|
|
}}
|
|
class="px-3 py-2 text-sm rounded-lg border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-secondary)] hover:border-red-400 hover:text-red-400 transition-colors"
|
|
>
|
|
Reset
|
|
</button>
|
|
</div>
|
|
{#if customFontStatus}
|
|
<p class="text-xs text-[var(--text-tertiary)]">{customFontStatus}</p>
|
|
{/if}
|
|
</div>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1">
|
|
Supports Google Fonts URLs, direct font file URLs, or local file paths. Family name is
|
|
required to apply the font.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Custom UI Font -->
|
|
<div class="mb-4">
|
|
<span class="block text-sm text-[var(--text-secondary)] mb-2">Custom UI Font</span>
|
|
<div class="flex flex-col gap-2">
|
|
<input
|
|
type="text"
|
|
bind:value={customUiFontPathInput}
|
|
placeholder="URL or local file path (e.g. /path/to/font.ttf)"
|
|
class="w-full px-3 py-2 text-sm rounded-lg border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-primary)] placeholder-gray-500 focus:outline-none focus:border-[var(--accent-primary)]"
|
|
/>
|
|
<input
|
|
type="text"
|
|
bind:value={customUiFontFamilyInput}
|
|
placeholder="Font family name (e.g. Inter)"
|
|
class="w-full px-3 py-2 text-sm rounded-lg border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-primary)] placeholder-gray-500 focus:outline-none focus:border-[var(--accent-primary)]"
|
|
/>
|
|
<div class="flex gap-2">
|
|
<button
|
|
onclick={async () => {
|
|
customUiFontStatus = null;
|
|
try {
|
|
await configStore.setCustomUiFont(
|
|
customUiFontPathInput || null,
|
|
customUiFontFamilyInput || null
|
|
);
|
|
customUiFontStatus = "Font applied!";
|
|
} catch (e) {
|
|
customUiFontStatus = `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
}
|
|
}}
|
|
class="flex-1 px-3 py-2 text-sm rounded-lg border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)] transition-colors"
|
|
>
|
|
Apply UI Font
|
|
</button>
|
|
<button
|
|
onclick={async () => {
|
|
customUiFontStatus = null;
|
|
customUiFontPathInput = "";
|
|
customUiFontFamilyInput = "";
|
|
try {
|
|
await configStore.setCustomUiFont(null, null);
|
|
await applyCustomUiFont(null, null);
|
|
customUiFontStatus = "Font reset to default.";
|
|
} catch (e) {
|
|
customUiFontStatus = `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
}
|
|
}}
|
|
class="px-3 py-2 text-sm rounded-lg border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-secondary)] hover:border-red-400 hover:text-red-400 transition-colors"
|
|
>
|
|
Reset
|
|
</button>
|
|
</div>
|
|
{#if customUiFontStatus}
|
|
<p class="text-xs text-[var(--text-tertiary)]">{customUiFontStatus}</p>
|
|
{/if}
|
|
</div>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1">
|
|
Applies to the entire app interface (menus, labels, buttons). Supports Google Fonts URLs,
|
|
direct font file URLs, or local file paths.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Show Thinking Blocks Toggle -->
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={config.show_thinking_blocks}
|
|
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Show Extended Thinking Blocks</span>
|
|
</label>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
Display Claude's extended thinking process in the conversation. Thinking blocks can be
|
|
expanded/collapsed to see reasoning details.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Background Image -->
|
|
<div class="mb-4">
|
|
<span class="block text-sm text-[var(--text-secondary)] mb-2">Background Image</span>
|
|
{#if config.background_image_path}
|
|
<p class="text-xs text-[var(--text-tertiary)] font-mono mb-2 truncate">
|
|
{config.background_image_path.split("/").pop()}
|
|
</p>
|
|
{/if}
|
|
<div class="flex gap-2">
|
|
<button
|
|
onclick={pickBackgroundImage}
|
|
class="flex-1 px-3 py-2 text-sm rounded-lg border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)] transition-colors"
|
|
>
|
|
{config.background_image_path ? "Change Image" : "Choose Image"}
|
|
</button>
|
|
{#if config.background_image_path}
|
|
<button
|
|
onclick={clearBackgroundImage}
|
|
class="px-3 py-2 text-sm rounded-lg border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-secondary)] hover:border-red-400 hover:text-red-400 transition-colors"
|
|
title="Remove background image"
|
|
>
|
|
Clear
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
{#if config.background_image_path}
|
|
<div class="mt-3">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<label for="bg-opacity" class="text-xs text-[var(--text-secondary)]"> Opacity </label>
|
|
<span class="text-xs text-[var(--text-tertiary)]">
|
|
{Math.round(config.background_image_opacity * 100)}%
|
|
</span>
|
|
</div>
|
|
<input
|
|
id="bg-opacity"
|
|
type="range"
|
|
bind:value={config.background_image_opacity}
|
|
min="0.05"
|
|
max="1"
|
|
step="0.05"
|
|
class="w-full h-2 bg-[var(--bg-primary)] rounded-lg appearance-none cursor-pointer"
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Window Section -->
|
|
<section class="mb-6">
|
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
|
Window
|
|
</h3>
|
|
|
|
<!-- Always on Top Toggle -->
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={config.always_on_top}
|
|
onchange={(e) => handleAlwaysOnTopChange(e.currentTarget.checked)}
|
|
class="w-4 h-4 text-[var(--accent-primary)] bg-[var(--bg-primary)] border-[var(--border-color)] rounded focus:ring-[var(--accent-primary)] focus:ring-2"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Always on top</span>
|
|
</label>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
Keep the window above other windows
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Update Checks Toggle -->
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={config.update_checks_enabled}
|
|
class="w-4 h-4 text-[var(--accent-primary)] bg-[var(--bg-primary)] border-[var(--border-color)] rounded focus:ring-[var(--accent-primary)] focus:ring-2"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Check for updates on startup</span>
|
|
</label>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
Notify when a new version is available
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Privacy / Streamer Mode Section -->
|
|
<section class="mb-6">
|
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
|
Privacy / Streamer Mode
|
|
</h3>
|
|
|
|
<!-- Streamer Mode Toggle -->
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={config.streamer_mode}
|
|
class="w-4 h-4 text-[var(--accent-primary)] bg-[var(--bg-primary)] border-[var(--border-color)] rounded focus:ring-[var(--accent-primary)] focus:ring-2"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Enable streamer mode</span>
|
|
</label>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
Hide sensitive information like API keys when streaming (Ctrl+Shift+S to toggle)
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Hide Paths Toggle -->
|
|
{#if config.streamer_mode}
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={config.streamer_hide_paths}
|
|
class="w-4 h-4 text-[var(--accent-primary)] bg-[var(--bg-primary)] border-[var(--border-color)] rounded focus:ring-[var(--accent-primary)] focus:ring-2"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Also hide file paths</span>
|
|
</label>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
Mask directory paths (e.g., /home/user → /home/****)
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
</section>
|
|
|
|
<!-- Budget Settings Section -->
|
|
<section class="mb-6">
|
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
|
Budget Settings
|
|
</h3>
|
|
|
|
<!-- Enable Budget Tracking -->
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={config.budget_enabled}
|
|
class="w-4 h-4 text-[var(--accent-primary)] bg-[var(--bg-primary)] border-[var(--border-color)] rounded focus:ring-[var(--accent-primary)] focus:ring-2"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Enable budget tracking</span>
|
|
</label>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
Set limits on token usage and costs per session
|
|
</p>
|
|
</div>
|
|
|
|
{#if config.budget_enabled}
|
|
<!-- Token Budget -->
|
|
<div class="mb-4">
|
|
<label for="token-budget" class="block text-sm text-[var(--text-secondary)] mb-1">
|
|
Session Token Budget
|
|
</label>
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
id="token-budget"
|
|
type="number"
|
|
bind:value={config.session_token_budget}
|
|
min="0"
|
|
step="10000"
|
|
placeholder="e.g., 100000"
|
|
class="flex-1 px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] text-sm focus:outline-none focus:border-[var(--accent-primary)]"
|
|
/>
|
|
<span class="text-xs text-[var(--text-tertiary)]">tokens</span>
|
|
</div>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1">Leave empty for unlimited tokens</p>
|
|
</div>
|
|
|
|
<!-- Cost Budget -->
|
|
<div class="mb-4">
|
|
<label for="cost-budget" class="block text-sm text-[var(--text-secondary)] mb-1">
|
|
Session Cost Budget
|
|
</label>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm text-[var(--text-secondary)]">$</span>
|
|
<input
|
|
id="cost-budget"
|
|
type="number"
|
|
bind:value={config.session_cost_budget}
|
|
min="0"
|
|
step="0.50"
|
|
placeholder="e.g., 5.00"
|
|
class="flex-1 px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] text-sm focus:outline-none focus:border-[var(--accent-primary)]"
|
|
/>
|
|
<span class="text-xs text-[var(--text-tertiary)]">USD</span>
|
|
</div>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1">Leave empty for unlimited spending</p>
|
|
</div>
|
|
|
|
<!-- Warning Threshold -->
|
|
<div class="mb-4">
|
|
<label for="warning-threshold" class="block text-sm text-[var(--text-secondary)] mb-2">
|
|
Warning Threshold
|
|
</label>
|
|
<div class="flex items-center gap-3">
|
|
<input
|
|
id="warning-threshold"
|
|
type="range"
|
|
bind:value={config.budget_warning_threshold}
|
|
min="0.5"
|
|
max="0.95"
|
|
step="0.05"
|
|
class="flex-1 h-2 bg-[var(--bg-primary)] rounded-lg appearance-none cursor-pointer"
|
|
/>
|
|
<span class="text-sm text-[var(--text-secondary)] w-12 text-right">
|
|
{Math.round(config.budget_warning_threshold * 100)}%
|
|
</span>
|
|
</div>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-1">
|
|
Show warning when this percentage of budget is used
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Budget Action -->
|
|
<div class="mb-4">
|
|
<span class="block text-sm text-[var(--text-secondary)] mb-2"
|
|
>When budget is exceeded</span
|
|
>
|
|
<div class="flex gap-2" role="group" aria-label="Budget action">
|
|
<button
|
|
onclick={() => (config.budget_action = "warn")}
|
|
class="flex-1 px-3 py-2 rounded-lg border transition-colors text-sm {config.budget_action ===
|
|
'warn'
|
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
|
>
|
|
Warn Only
|
|
</button>
|
|
<button
|
|
onclick={() => (config.budget_action = "block")}
|
|
class="flex-1 px-3 py-2 rounded-lg border transition-colors text-sm {config.budget_action ===
|
|
'block'
|
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
|
>
|
|
Block Input
|
|
</button>
|
|
</div>
|
|
<p class="text-xs text-[var(--text-tertiary)] mt-2">
|
|
{config.budget_action === "warn"
|
|
? "Show a warning but allow continued usage"
|
|
: "Prevent sending more messages until session is reset"}
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
</section>
|
|
|
|
<!-- Cost History Section -->
|
|
<section class="mb-6">
|
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
|
Cost History
|
|
</h3>
|
|
<CostSummary />
|
|
</section>
|
|
|
|
<!-- Notifications Section -->
|
|
<section class="mb-6">
|
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
|
Notifications
|
|
</h3>
|
|
|
|
<!-- Enable/Disable Notifications -->
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={config.notifications_enabled}
|
|
class="w-4 h-4 text-[var(--accent-primary)] bg-[var(--bg-primary)] border-[var(--border-color)] rounded focus:ring-[var(--accent-primary)] focus:ring-2"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Enable sound notifications</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Volume Control -->
|
|
<div class="mb-4">
|
|
<label for="notification-volume" class="block text-sm text-[var(--text-secondary)] mb-2">
|
|
Notification Volume
|
|
</label>
|
|
<div class="flex items-center gap-3">
|
|
<input
|
|
id="notification-volume"
|
|
type="range"
|
|
bind:value={config.notification_volume}
|
|
min="0"
|
|
max="1"
|
|
step="0.1"
|
|
disabled={!config.notifications_enabled}
|
|
class="flex-1 h-2 bg-[var(--bg-primary)] rounded-lg appearance-none cursor-pointer disabled:opacity-50"
|
|
/>
|
|
<span class="text-sm text-gray-300 w-12 text-right">
|
|
{Math.round(config.notification_volume * 100)}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-xs text-[var(--text-tertiary)]">
|
|
Sound notifications will play when I complete tasks, encounter errors, or need permissions.
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Discord Rich Presence Section -->
|
|
<section class="pt-6 pb-6 border-t border-[var(--border-color)]">
|
|
<h3 class="text-lg font-semibold text-[var(--accent-primary)] mb-4 flex items-center gap-2">
|
|
<span>🎮</span>
|
|
<span>Discord Rich Presence</span>
|
|
</h3>
|
|
|
|
<!-- Enable/Disable Discord RPC -->
|
|
<div class="mb-4">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={config.discord_rpc_enabled}
|
|
class="w-4 h-4 text-[var(--accent-primary)] bg-[var(--bg-primary)] border-[var(--border-color)] rounded focus:ring-[var(--accent-primary)] focus:ring-2"
|
|
/>
|
|
<span class="text-sm text-[var(--text-primary)]">Show activity in Discord</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="text-xs text-[var(--text-tertiary)]">
|
|
Display your current conversation session name and model in Discord when enabled.
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Save Button -->
|
|
<div class="sticky bottom-0 pt-4 pb-2 bg-[var(--bg-secondary)]">
|
|
<button
|
|
onclick={handleSave}
|
|
disabled={isSaving}
|
|
class="btn-trans-gradient w-full py-3 font-medium rounded-lg"
|
|
>
|
|
{isSaving ? "Saving..." : "Save Settings"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<style>
|
|
/* Custom range input styling */
|
|
input[type="range"]::-webkit-slider-thumb {
|
|
appearance: none;
|
|
width: 16px;
|
|
height: 16px;
|
|
background: var(--accent-primary);
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
}
|
|
|
|
input[type="range"]::-moz-range-thumb {
|
|
width: 16px;
|
|
height: 16px;
|
|
background: var(--accent-primary);
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
border: none;
|
|
}
|
|
|
|
input[type="range"]:disabled::-webkit-slider-thumb {
|
|
background: var(--text-tertiary);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
input[type="range"]:disabled::-moz-range-thumb {
|
|
background: var(--text-tertiary);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* Color picker styling */
|
|
.color-input-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.color-picker {
|
|
width: 32px;
|
|
height: 32px;
|
|
padding: 0;
|
|
border: 2px solid var(--border-color);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
background: transparent;
|
|
}
|
|
|
|
.color-picker::-webkit-color-swatch-wrapper {
|
|
padding: 2px;
|
|
}
|
|
|
|
.color-picker::-webkit-color-swatch {
|
|
border-radius: 4px;
|
|
border: none;
|
|
}
|
|
|
|
.color-picker::-moz-color-swatch {
|
|
border-radius: 4px;
|
|
border: none;
|
|
}
|
|
|
|
.color-picker:hover {
|
|
border-color: var(--accent-primary);
|
|
}
|
|
</style>
|