generated from nhcarrigan/template
b745100bd5
## Summary This PR covers the full audit of Claude CLI changes from 2.1.50 to 2.1.53, plus a batch of bug fixes, new features, and maintenance work identified during that review. ### New Features - **Workspace trust gate** — detects hooks, MCP servers, and custom commands in a workspace before connecting; persists trust decisions so users aren't prompted repeatedly - **Custom background image** — users can set a background image with configurable opacity; character panel and compact mode go transparent when active - **Draggable tab reordering** — conversation tabs can be reordered via pointer-event drag-and-drop (HTML5 drag is intercepted by Tauri/WebView2, so pointer events are used instead) - **Org UUID in account info** — exposes the org UUID from Claude auth status ### Bug Fixes - **Unread dot false positives** — initialise unread counts on mount to prevent all tabs showing the blue dot after toggling the file editor (Closes #164) - **Watchdog for hung WSL bridge** — detects connections that never receive `system:init` and kills the stale process after 1 minute (Closes #166) - **Suppress terminal window flash on Windows** — applies `CREATE_NO_WINDOW` to all subprocesses via a `HideWindow` trait extension (Closes #165) - **HTML escaping in markdown renderer** — escape `<` and `>` in `codespan` and `html` renderer callbacks to prevent raw HTML injection (Closes #169) ### Maintenance - Verify stream-JSON handles tool results above the 50K threshold correctly (Closes #162) - Reviewed hook security fixes from CLI 2.1.51 — not applicable to our setup (Closes #163) - Expose org UUID from `claude auth status` (Closes #160) - Clean up Svelte and Vite build warnings (`a11y_click_events_have_key_events`, `state_referenced_locally`, `non_reactive_update`, `codeSplitting`, chunk size, CodeMirror dynamic import) - Update all npm dependencies to latest compatible versions with exact pinning (Closes #81, Closes #82, Closes #83, Closes #84, Closes #85, Closes #86, Closes #87, Closes #90, Closes #91, Closes #93, Closes #94, Closes #95, Closes #96, Closes #97, Closes #98, Closes #99, Closes #101, Closes #141, Closes #142, Closes #143, Closes #145, Closes #146, Closes #147) - Run `cargo update` to bring Cargo.lock up to date ### Closes Closes #160 Closes #162 Closes #163 Closes #164 Closes #165 Closes #166 Closes #167 Closes #168 Closes #169 Closes #81 Closes #82 Closes #83 Closes #84 Closes #85 Closes #86 Closes #87 Closes #90 Closes #91 Closes #93 Closes #94 Closes #95 Closes #96 Closes #97 Closes #98 Closes #99 Closes #101 Closes #141 Closes #142 Closes #143 Closes #145 Closes #146 Closes #147 ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #171 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
769 lines
24 KiB
Svelte
769 lines
24 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from "svelte";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { listen } from "@tauri-apps/api/event";
|
|
import { get } from "svelte/store";
|
|
import {
|
|
initializeTauriListeners,
|
|
cleanupTauriListeners,
|
|
initializeDiscordRpc,
|
|
stopDiscordRpc,
|
|
updateDiscordRpc,
|
|
setSkipNextGreeting,
|
|
} from "$lib/tauri";
|
|
import { configStore, applyTheme, applyFontSize, isCompactMode } from "$lib/stores/config";
|
|
import { readFile } from "@tauri-apps/plugin-fs";
|
|
import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications";
|
|
import { conversationsStore } from "$lib/stores/conversations";
|
|
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
|
|
import { editorStore } from "$lib/stores/editor";
|
|
import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window";
|
|
import "$lib/notifications/testNotifications";
|
|
import Terminal from "$lib/components/Terminal.svelte";
|
|
import InputBar from "$lib/components/InputBar.svelte";
|
|
import StatusBar from "$lib/components/StatusBar.svelte";
|
|
import AnimeGirl from "$lib/components/AnimeGirl.svelte";
|
|
import CompactMode from "$lib/components/CompactMode.svelte";
|
|
import EditorPanel from "$lib/components/editor/EditorPanel.svelte";
|
|
import { characterState } from "$lib/stores/character";
|
|
import type { CharacterState } from "$lib/types/states";
|
|
import PermissionModal from "$lib/components/PermissionModal.svelte";
|
|
import UserQuestionModal from "$lib/components/UserQuestionModal.svelte";
|
|
import ConfigSidebar from "$lib/components/ConfigSidebar.svelte";
|
|
import AchievementNotification from "$lib/components/AchievementNotification.svelte";
|
|
import AchievementsPanel from "$lib/components/AchievementsPanel.svelte";
|
|
import UpdateNotification from "$lib/components/UpdateNotification.svelte";
|
|
import CloseAppConfirmModal from "$lib/components/CloseAppConfirmModal.svelte";
|
|
import MemoryBrowserPanel from "$lib/components/MemoryBrowserPanel.svelte";
|
|
import { debugConsoleStore } from "$lib/stores/debugConsole";
|
|
import { initializeTodoListener, cleanupTodoListener } from "$lib/stores/todos";
|
|
|
|
let backgroundDataUrl = $state<string | null>(null);
|
|
let backgroundOpacity = $state(0.3);
|
|
|
|
const configValues = configStore.config;
|
|
$effect(() => {
|
|
const cfg = $configValues;
|
|
backgroundOpacity = cfg.background_image_opacity;
|
|
if (cfg.background_image_path) {
|
|
void loadBackgroundImage(cfg.background_image_path);
|
|
} else {
|
|
backgroundDataUrl = null;
|
|
}
|
|
});
|
|
|
|
async function loadBackgroundImage(path: string) {
|
|
try {
|
|
const data = await readFile(path);
|
|
const chunks: string[] = [];
|
|
const chunkSize = 8192;
|
|
for (let i = 0; i < data.length; i += chunkSize) {
|
|
chunks.push(String.fromCharCode(...data.slice(i, i + chunkSize)));
|
|
}
|
|
const ext = path.split(".").pop()?.toLowerCase() ?? "png";
|
|
const mimeMap: Record<string, string> = {
|
|
jpg: "image/jpeg",
|
|
jpeg: "image/jpeg",
|
|
png: "image/png",
|
|
webp: "image/webp",
|
|
gif: "image/gif",
|
|
avif: "image/avif",
|
|
};
|
|
const mime = mimeMap[ext] ?? "image/png";
|
|
backgroundDataUrl = `data:${mime};base64,${btoa(chunks.join(""))}`;
|
|
} catch (error) {
|
|
console.error("Failed to load background image:", error);
|
|
backgroundDataUrl = null;
|
|
}
|
|
}
|
|
|
|
let initialized = false;
|
|
let updateNotification: UpdateNotification | undefined = $state(undefined);
|
|
let achievementPanelOpen = $state(false);
|
|
let currentCharacterState: CharacterState = $state("idle");
|
|
let compactModeActive = $state(false);
|
|
let closeConfirmModalOpen = $state(false);
|
|
let hasActiveConversation = $state(false);
|
|
|
|
// Editor state
|
|
const isEditorVisible = editorStore.isEditorVisible;
|
|
let lastInitializedCwd = "";
|
|
|
|
// Track connection status and CWD for the editor
|
|
const connectionStatus = claudeStore.connectionStatus;
|
|
const currentWorkingDirectory = claudeStore.currentWorkingDirectory;
|
|
|
|
// Initialize/update editor file tree when CWD changes while editor is visible
|
|
$effect(() => {
|
|
const visible = $isEditorVisible;
|
|
const cwd = $currentWorkingDirectory;
|
|
const connected = $connectionStatus === "connected";
|
|
|
|
// Only initialize when editor is visible, connected, and CWD is set
|
|
if (visible && connected && cwd && cwd !== lastInitializedCwd) {
|
|
lastInitializedCwd = cwd;
|
|
editorStore.initializeFileTree(cwd);
|
|
}
|
|
|
|
// Hide editor if disconnected
|
|
if (!connected && visible) {
|
|
editorStore.hideEditor();
|
|
}
|
|
});
|
|
|
|
// Get reactive references to conversation stores
|
|
const activeConversationId = conversationsStore.activeConversationId;
|
|
const conversations = conversationsStore.conversations;
|
|
|
|
// Update Discord RPC when active conversation or model changes
|
|
$effect(() => {
|
|
// Access stores directly (without get()) to create reactive dependencies
|
|
const activeId = $activeConversationId;
|
|
const convs = $conversations;
|
|
const activeConv = activeId ? convs.get(activeId) : null;
|
|
const config = configStore.getConfig();
|
|
const model = config.model || "claude";
|
|
|
|
if (activeConv && config.discord_rpc_enabled) {
|
|
updateDiscordRpc(activeConv.name, model, activeConv.startedAt);
|
|
}
|
|
});
|
|
|
|
// Window size constants
|
|
const COMPACT_WIDTH = 280;
|
|
const COMPACT_HEIGHT = 400;
|
|
|
|
// Store the previous window size to restore when exiting compact mode
|
|
let previousWindowSize: { width: number; height: number } | null = null;
|
|
|
|
characterState.subscribe((state) => {
|
|
currentCharacterState = state;
|
|
});
|
|
|
|
isCompactMode.subscribe((value) => {
|
|
compactModeActive = value;
|
|
});
|
|
|
|
function getPanelGlowClass(): string {
|
|
switch (currentCharacterState) {
|
|
case "thinking":
|
|
return "panel-glow-thinking";
|
|
case "typing":
|
|
return "panel-glow-typing";
|
|
case "searching":
|
|
return "panel-glow-searching";
|
|
case "coding":
|
|
return "panel-glow-coding";
|
|
case "mcp":
|
|
return "panel-glow-mcp";
|
|
case "success":
|
|
return "panel-glow-success";
|
|
case "error":
|
|
return "panel-glow-error";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
// Resizable panel state
|
|
let panelWidth = $state(320); // Default width in pixels
|
|
let isResizing = $state(false);
|
|
const MIN_PANEL_WIDTH = 200;
|
|
const MAX_PANEL_WIDTH = 600;
|
|
|
|
function startResize(event: MouseEvent) {
|
|
isResizing = true;
|
|
event.preventDefault();
|
|
document.addEventListener("mousemove", handleResize);
|
|
document.addEventListener("mouseup", stopResize);
|
|
}
|
|
|
|
function handleResize(event: MouseEvent) {
|
|
if (!isResizing) return;
|
|
const newWidth = event.clientX;
|
|
panelWidth = Math.max(MIN_PANEL_WIDTH, Math.min(MAX_PANEL_WIDTH, newWidth));
|
|
}
|
|
|
|
function stopResize() {
|
|
if (isResizing) {
|
|
isResizing = false;
|
|
document.removeEventListener("mousemove", handleResize);
|
|
document.removeEventListener("mouseup", stopResize);
|
|
// Save the panel width to config
|
|
configStore.updateConfig({ character_panel_width: panelWidth });
|
|
}
|
|
}
|
|
|
|
// Global keyboard shortcuts
|
|
function handleGlobalKeydown(event: KeyboardEvent) {
|
|
// Don't trigger shortcuts when typing in inputs (except for specific ones)
|
|
const target = event.target as HTMLElement;
|
|
const isInputFocused = target.tagName === "INPUT" || target.tagName === "TEXTAREA";
|
|
|
|
// Escape closes panels (always works)
|
|
if (event.key === "Escape") {
|
|
// Check if any panels are open and close them
|
|
if (achievementPanelOpen) {
|
|
achievementPanelOpen = false;
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
// ConfigSidebar handles its own escape via store
|
|
if (get(configStore.isSidebarOpen)) {
|
|
configStore.closeSidebar();
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Skip other shortcuts if user is typing in an input
|
|
if (isInputFocused) return;
|
|
|
|
// Ctrl+L - Clear terminal
|
|
if (event.ctrlKey && event.key === "l") {
|
|
event.preventDefault();
|
|
claudeStore.clearTerminal();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+, - Open settings
|
|
if (event.ctrlKey && event.key === ",") {
|
|
event.preventDefault();
|
|
configStore.openSidebar();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+C - Interrupt (only when processing)
|
|
if (event.ctrlKey && event.key === "c") {
|
|
if (get(isClaudeProcessing)) {
|
|
event.preventDefault();
|
|
handleInterrupt();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Ctrl++ or Ctrl+= - Increase font size
|
|
if (event.ctrlKey && (event.key === "+" || event.key === "=")) {
|
|
event.preventDefault();
|
|
configStore.increaseFontSize();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+- - Decrease font size
|
|
if (event.ctrlKey && event.key === "-") {
|
|
event.preventDefault();
|
|
configStore.decreaseFontSize();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+0 - Reset font size
|
|
if (event.ctrlKey && event.key === "0") {
|
|
event.preventDefault();
|
|
configStore.resetFontSize();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+Shift+S - Toggle streamer mode
|
|
if (event.ctrlKey && event.shiftKey && event.key === "S") {
|
|
event.preventDefault();
|
|
configStore.toggleStreamerMode();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+Shift+M - Toggle compact mode
|
|
if (event.ctrlKey && event.shiftKey && event.key === "M") {
|
|
event.preventDefault();
|
|
toggleCompactMode();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+` - Toggle debug console
|
|
if (event.ctrlKey && event.key === "`") {
|
|
event.preventDefault();
|
|
debugConsoleStore.toggle();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+E - Toggle editor panel (only when connected)
|
|
if (event.ctrlKey && event.key === "e") {
|
|
event.preventDefault();
|
|
// Only allow opening the editor when connected
|
|
if (get(claudeStore.connectionStatus) === "connected") {
|
|
editorStore.toggleEditor();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Ctrl+B - Toggle file browser (when editor is visible)
|
|
if (event.ctrlKey && event.key === "b" && get(editorStore.isEditorVisible)) {
|
|
event.preventDefault();
|
|
editorStore.toggleFileBrowser();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+S - Save current file (when editor is visible)
|
|
if (event.ctrlKey && event.key === "s" && get(editorStore.isEditorVisible)) {
|
|
event.preventDefault();
|
|
editorStore.saveFile();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+W - Close current tab (when editor is visible)
|
|
if (event.ctrlKey && event.key === "w" && get(editorStore.isEditorVisible)) {
|
|
event.preventDefault();
|
|
const activeTabId = get(editorStore.activeTabId);
|
|
if (activeTabId) {
|
|
editorStore.closeTab(activeTabId);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Ctrl+N - New file (when editor is visible)
|
|
// Note: This just emits an event that FileBrowser listens to
|
|
if (event.ctrlKey && event.key === "n" && get(editorStore.isEditorVisible)) {
|
|
event.preventDefault();
|
|
// Dispatch a custom event that FileBrowser will listen to
|
|
window.dispatchEvent(new CustomEvent("editor-new-file"));
|
|
return;
|
|
}
|
|
}
|
|
|
|
async function handleInterrupt() {
|
|
try {
|
|
const conversationId = get(claudeStore.activeConversationId);
|
|
if (!conversationId) return;
|
|
|
|
// Set flag to preserve stats/permissions (don't treat next connect as new session)
|
|
setSkipNextGreeting(true);
|
|
|
|
await invoke("interrupt_claude", { conversationId });
|
|
claudeStore.addLine("system", "Process interrupted");
|
|
} catch (error) {
|
|
console.error("Failed to interrupt:", error);
|
|
}
|
|
}
|
|
|
|
async function enterCompactMode() {
|
|
try {
|
|
const window = getCurrentWindow();
|
|
const currentSize = await window.innerSize();
|
|
const scaleFactor = await window.scaleFactor();
|
|
|
|
// Save current window size (convert from physical to logical pixels)
|
|
// innerSize() returns physical pixels, but setSize() with LogicalSize expects logical
|
|
previousWindowSize = {
|
|
width: Math.round(currentSize.width / scaleFactor),
|
|
height: Math.round(currentSize.height / scaleFactor),
|
|
};
|
|
|
|
// Resize to compact dimensions
|
|
await window.setSize(new LogicalSize(COMPACT_WIDTH, COMPACT_HEIGHT));
|
|
|
|
// Enable compact mode in config
|
|
await configStore.setCompactMode(true);
|
|
} catch (error) {
|
|
console.error("Failed to enter compact mode:", error);
|
|
}
|
|
}
|
|
|
|
async function exitCompactMode() {
|
|
try {
|
|
const window = getCurrentWindow();
|
|
|
|
// Only resize if we have a saved previous size
|
|
// (i.e., user entered compact mode during this session)
|
|
// Otherwise, just expand to a reasonable default
|
|
if (previousWindowSize) {
|
|
await window.setSize(new LogicalSize(previousWindowSize.width, previousWindowSize.height));
|
|
previousWindowSize = null;
|
|
} else {
|
|
// No saved size (e.g., app started in compact mode) - use modest default
|
|
await window.setSize(new LogicalSize(900, 650));
|
|
}
|
|
|
|
// Disable compact mode in config
|
|
await configStore.setCompactMode(false);
|
|
} catch (error) {
|
|
console.error("Failed to exit compact mode:", error);
|
|
}
|
|
}
|
|
|
|
async function toggleCompactMode() {
|
|
if (compactModeActive) {
|
|
await exitCompactMode();
|
|
} else {
|
|
await enterCompactMode();
|
|
}
|
|
}
|
|
|
|
async function handleCloseRequest() {
|
|
// Check if there's an active conversation with Claude running
|
|
const activeId = get(claudeStore.activeConversationId);
|
|
if (activeId) {
|
|
try {
|
|
const isRunning = await invoke<boolean>("is_claude_running", {
|
|
conversationId: activeId,
|
|
});
|
|
hasActiveConversation = isRunning;
|
|
} catch (error) {
|
|
console.error("Failed to check Claude status:", error);
|
|
hasActiveConversation = false;
|
|
}
|
|
} else {
|
|
hasActiveConversation = false;
|
|
}
|
|
|
|
// Always show confirmation modal
|
|
closeConfirmModalOpen = true;
|
|
}
|
|
|
|
async function handleConfirmClose() {
|
|
closeConfirmModalOpen = false;
|
|
try {
|
|
await invoke("close_application");
|
|
} catch (error) {
|
|
console.error("Failed to close application:", error);
|
|
}
|
|
}
|
|
|
|
async function handleMinimizeToTray() {
|
|
closeConfirmModalOpen = false;
|
|
try {
|
|
const window = getCurrentWindow();
|
|
await window.hide();
|
|
} catch (error) {
|
|
console.error("Failed to minimize to tray:", error);
|
|
}
|
|
}
|
|
|
|
function handleCancelClose() {
|
|
closeConfirmModalOpen = false;
|
|
}
|
|
|
|
onMount(async () => {
|
|
if (!initialized) {
|
|
initialized = true;
|
|
|
|
// Initialize conversations store first to ensure activeConversationId is set
|
|
conversationsStore.initialize();
|
|
|
|
await initializeTauriListeners();
|
|
await configStore.loadConfig();
|
|
|
|
// Apply saved settings on startup
|
|
const config = configStore.getConfig();
|
|
applyTheme(config.theme, config.custom_theme_colors);
|
|
applyFontSize(config.font_size);
|
|
|
|
// Apply always-on-top setting
|
|
if (config.always_on_top) {
|
|
const window = getCurrentWindow();
|
|
await window.setAlwaysOnTop(true);
|
|
}
|
|
|
|
// Load saved panel width
|
|
if (config.character_panel_width) {
|
|
panelWidth = config.character_panel_width;
|
|
}
|
|
|
|
// Initialize notification settings sync
|
|
initNotificationSync();
|
|
|
|
// Add global keyboard shortcut listener
|
|
window.addEventListener("keydown", handleGlobalKeydown);
|
|
|
|
// Check for updates on startup
|
|
if (config.update_checks_enabled) {
|
|
updateNotification?.checkForUpdates();
|
|
}
|
|
|
|
// Apply compact mode if saved (resize window)
|
|
if (config.compact_mode) {
|
|
const window = getCurrentWindow();
|
|
await window.setSize(new LogicalSize(COMPACT_WIDTH, COMPACT_HEIGHT));
|
|
}
|
|
|
|
// Initialize Discord RPC
|
|
await initializeDiscordRpc();
|
|
|
|
// Initialize todo listener
|
|
await initializeTodoListener();
|
|
|
|
// Listen for window close requests
|
|
const unlisten = await listen("window-close-requested", () => {
|
|
handleCloseRequest();
|
|
});
|
|
|
|
// Store the unlisten function for cleanup
|
|
window.addEventListener("beforeunload", () => {
|
|
unlisten();
|
|
});
|
|
}
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (initialized) {
|
|
cleanupTauriListeners();
|
|
cleanupNotificationSync();
|
|
cleanupTodoListener();
|
|
stopDiscordRpc();
|
|
window.removeEventListener("keydown", handleGlobalKeydown);
|
|
initialized = false;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{#if backgroundDataUrl}
|
|
<div
|
|
class="fixed inset-0 bg-cover bg-center pointer-events-none"
|
|
style="background-image: url('{backgroundDataUrl}'); opacity: {backgroundOpacity}; z-index: 0;"
|
|
></div>
|
|
{/if}
|
|
|
|
{#if compactModeActive}
|
|
<!-- Compact mode: minimal widget interface -->
|
|
<div
|
|
class="app-container compact-app h-screen w-screen flex flex-col bg-[var(--bg-primary)] overflow-hidden"
|
|
style={backgroundDataUrl ? "background: transparent;" : ""}
|
|
>
|
|
<CompactMode onExpand={exitCompactMode} />
|
|
</div>
|
|
{:else}
|
|
<!-- Full mode: standard interface -->
|
|
<div
|
|
class="app-container h-screen w-screen flex flex-col bg-[var(--bg-primary)] overflow-hidden"
|
|
style={backgroundDataUrl ? "background: transparent;" : ""}
|
|
>
|
|
<StatusBar
|
|
onToggleAchievements={() => (achievementPanelOpen = !achievementPanelOpen)}
|
|
onToggleCompact={enterCompactMode}
|
|
/>
|
|
|
|
<main class="flex-1 flex overflow-hidden">
|
|
<!-- Left panel: Character display -->
|
|
<div
|
|
class="character-panel {getPanelGlowClass()} flex flex-col items-center justify-center {backgroundDataUrl
|
|
? ''
|
|
: 'bg-[var(--bg-secondary)]/50'}"
|
|
style="width: {panelWidth}px; min-width: {MIN_PANEL_WIDTH}px; max-width: {MAX_PANEL_WIDTH}px;{backgroundDataUrl
|
|
? ' background: transparent !important;'
|
|
: ''}"
|
|
>
|
|
<AnimeGirl />
|
|
</div>
|
|
|
|
<!-- Resize handle -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class="resize-handle w-1 cursor-col-resize bg-[var(--border-color)] hover:bg-[var(--accent-primary)] transition-colors flex-shrink-0"
|
|
class:bg-[var(--accent-primary)]={isResizing}
|
|
onmousedown={startResize}
|
|
></div>
|
|
|
|
<!-- Right panel: Terminal/Editor and input -->
|
|
<div class="terminal-panel flex-1 flex flex-col min-w-0">
|
|
{#if $isEditorVisible}
|
|
<EditorPanel />
|
|
{:else}
|
|
<Terminal />
|
|
<InputBar />
|
|
{/if}
|
|
</div>
|
|
</main>
|
|
|
|
<PermissionModal />
|
|
<UserQuestionModal />
|
|
<ConfigSidebar />
|
|
<MemoryBrowserPanel />
|
|
<AchievementNotification />
|
|
<AchievementsPanel
|
|
bind:isOpen={achievementPanelOpen}
|
|
onClose={() => (achievementPanelOpen = false)}
|
|
/>
|
|
<UpdateNotification bind:this={updateNotification} />
|
|
<CloseAppConfirmModal
|
|
isOpen={closeConfirmModalOpen}
|
|
{hasActiveConversation}
|
|
onClose={handleConfirmClose}
|
|
onMinimize={handleMinimizeToTray}
|
|
onCancel={handleCancelClose}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.app-container {
|
|
font-family:
|
|
"Inter",
|
|
-apple-system,
|
|
BlinkMacSystemFont,
|
|
"Segoe UI",
|
|
Roboto,
|
|
sans-serif;
|
|
}
|
|
|
|
.character-panel {
|
|
background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-primary) 100%);
|
|
transition: all 0.5s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.character-panel::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
padding: 3px;
|
|
background: transparent;
|
|
-webkit-mask:
|
|
linear-gradient(#fff 0 0) content-box,
|
|
linear-gradient(#fff 0 0);
|
|
mask:
|
|
linear-gradient(#fff 0 0) content-box,
|
|
linear-gradient(#fff 0 0);
|
|
-webkit-mask-composite: xor;
|
|
mask-composite: exclude;
|
|
opacity: 0;
|
|
transition: opacity 0.5s ease;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Trans pride gradient glow effects for the character panel */
|
|
.panel-glow-thinking {
|
|
background: linear-gradient(
|
|
180deg,
|
|
color-mix(in srgb, var(--bg-secondary) 85%, #9333ea) 0%,
|
|
color-mix(in srgb, var(--bg-primary) 90%, var(--trans-blue)) 50%,
|
|
color-mix(in srgb, var(--bg-primary) 90%, var(--trans-pink)) 100%
|
|
);
|
|
box-shadow:
|
|
inset 0 0 60px rgba(147, 51, 234, 0.15),
|
|
inset 0 0 100px rgba(91, 206, 250, 0.1),
|
|
0 0 40px rgba(91, 206, 250, 0.2),
|
|
0 0 80px rgba(245, 169, 184, 0.15);
|
|
}
|
|
|
|
.panel-glow-thinking::before {
|
|
background: linear-gradient(180deg, #9333ea, var(--trans-blue), var(--trans-pink));
|
|
opacity: 1;
|
|
}
|
|
|
|
.panel-glow-typing {
|
|
background: linear-gradient(
|
|
180deg,
|
|
color-mix(in srgb, var(--bg-secondary) 85%, #3b82f6) 0%,
|
|
color-mix(in srgb, var(--bg-primary) 90%, var(--trans-blue)) 50%,
|
|
color-mix(in srgb, var(--bg-primary) 90%, var(--trans-pink)) 100%
|
|
);
|
|
box-shadow:
|
|
inset 0 0 60px rgba(59, 130, 246, 0.15),
|
|
inset 0 0 100px rgba(91, 206, 250, 0.15),
|
|
0 0 40px rgba(91, 206, 250, 0.25),
|
|
0 0 80px rgba(245, 169, 184, 0.15);
|
|
}
|
|
|
|
.panel-glow-typing::before {
|
|
background: linear-gradient(180deg, #3b82f6, var(--trans-blue), var(--trans-pink));
|
|
opacity: 1;
|
|
}
|
|
|
|
.panel-glow-searching {
|
|
background: linear-gradient(
|
|
180deg,
|
|
color-mix(in srgb, var(--bg-secondary) 85%, #eab308) 0%,
|
|
color-mix(in srgb, var(--bg-primary) 90%, var(--trans-blue)) 50%,
|
|
color-mix(in srgb, var(--bg-primary) 90%, var(--trans-pink)) 100%
|
|
);
|
|
box-shadow:
|
|
inset 0 0 60px rgba(234, 179, 8, 0.15),
|
|
inset 0 0 100px rgba(91, 206, 250, 0.1),
|
|
0 0 40px rgba(91, 206, 250, 0.2),
|
|
0 0 80px rgba(245, 169, 184, 0.15);
|
|
}
|
|
|
|
.panel-glow-searching::before {
|
|
background: linear-gradient(180deg, #eab308, var(--trans-blue), var(--trans-pink));
|
|
opacity: 1;
|
|
}
|
|
|
|
.panel-glow-coding {
|
|
background: linear-gradient(
|
|
180deg,
|
|
color-mix(in srgb, var(--bg-secondary) 85%, #22c55e) 0%,
|
|
color-mix(in srgb, var(--bg-primary) 90%, var(--trans-blue)) 50%,
|
|
color-mix(in srgb, var(--bg-primary) 90%, var(--trans-pink)) 100%
|
|
);
|
|
box-shadow:
|
|
inset 0 0 60px rgba(34, 197, 94, 0.15),
|
|
inset 0 0 100px rgba(91, 206, 250, 0.1),
|
|
0 0 40px rgba(91, 206, 250, 0.2),
|
|
0 0 80px rgba(245, 169, 184, 0.15);
|
|
}
|
|
|
|
.panel-glow-coding::before {
|
|
background: linear-gradient(180deg, #22c55e, var(--trans-blue), var(--trans-pink));
|
|
opacity: 1;
|
|
}
|
|
|
|
.panel-glow-mcp {
|
|
background: linear-gradient(
|
|
180deg,
|
|
color-mix(in srgb, var(--bg-secondary) 80%, var(--trans-blue)) 0%,
|
|
color-mix(in srgb, var(--bg-primary) 85%, var(--trans-pink)) 50%,
|
|
color-mix(in srgb, var(--bg-primary) 90%, var(--trans-blue)) 100%
|
|
);
|
|
box-shadow:
|
|
inset 0 0 80px rgba(91, 206, 250, 0.2),
|
|
inset 0 0 120px rgba(245, 169, 184, 0.15),
|
|
0 0 60px rgba(91, 206, 250, 0.3),
|
|
0 0 100px rgba(245, 169, 184, 0.2);
|
|
}
|
|
|
|
.panel-glow-mcp::before {
|
|
background: var(--trans-gradient-vibrant);
|
|
opacity: 1;
|
|
}
|
|
|
|
.panel-glow-success {
|
|
background: linear-gradient(
|
|
180deg,
|
|
color-mix(in srgb, var(--bg-secondary) 85%, #10b981) 0%,
|
|
color-mix(in srgb, var(--bg-primary) 90%, var(--trans-blue)) 50%,
|
|
color-mix(in srgb, var(--bg-primary) 90%, var(--trans-pink)) 100%
|
|
);
|
|
box-shadow:
|
|
inset 0 0 60px rgba(16, 185, 129, 0.15),
|
|
inset 0 0 100px rgba(91, 206, 250, 0.1),
|
|
0 0 40px rgba(91, 206, 250, 0.2),
|
|
0 0 80px rgba(245, 169, 184, 0.15);
|
|
}
|
|
|
|
.panel-glow-success::before {
|
|
background: linear-gradient(180deg, #10b981, var(--trans-blue), var(--trans-pink));
|
|
opacity: 1;
|
|
}
|
|
|
|
.panel-glow-error {
|
|
background: linear-gradient(
|
|
180deg,
|
|
color-mix(in srgb, var(--bg-secondary) 80%, #ef4444) 0%,
|
|
color-mix(in srgb, var(--bg-primary) 90%, var(--trans-pink)) 50%,
|
|
color-mix(in srgb, var(--bg-primary) 95%, var(--trans-blue)) 100%
|
|
);
|
|
box-shadow:
|
|
inset 0 0 60px rgba(239, 68, 68, 0.2),
|
|
inset 0 0 100px rgba(245, 169, 184, 0.1),
|
|
0 0 40px rgba(245, 169, 184, 0.2),
|
|
0 0 80px rgba(239, 68, 68, 0.15);
|
|
}
|
|
|
|
.panel-glow-error::before {
|
|
background: linear-gradient(180deg, #ef4444, var(--trans-pink), var(--trans-blue));
|
|
opacity: 1;
|
|
}
|
|
|
|
.resize-handle:hover,
|
|
.resize-handle:active {
|
|
width: 4px;
|
|
}
|
|
</style>
|