feat: add about and help panels, donate button, and live setting update (#48)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 52s
CI / Lint & Test (push) Successful in 14m11s
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled

### Explanation

_No response_

### Issue

Closes #26 Closes #27

### Attestations

- [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)
- [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
- [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/).

### Dependencies

- [ ] I have pinned the dependencies to a specific patch version.

### Style

- [ ] I have run the linter and resolved any errors.
- [ ] My pull request uses an appropriate title, matching the conventional commit standards.
- [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request.

### Tests

- [ ] My contribution adds new code, and I have added tests to cover it.
- [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes.
- [ ] All new and existing tests pass locally with my changes.
- [ ] Code coverage remains at or above the configured threshold.

### Documentation

_No response_

### Versioning

_No response_

Reviewed-on: #48
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #48.
This commit is contained in:
2026-01-20 20:04:03 -08:00
committed by Naomi Carrigan
parent d83697e5cf
commit 377f81d978
6 changed files with 380 additions and 16 deletions
+151
View File
@@ -0,0 +1,151 @@
<script lang="ts">
import { openUrl } from "@tauri-apps/plugin-opener";
import { getVersion } from "@tauri-apps/api/app";
import { onMount } from "svelte";
interface Props {
onClose: () => void;
}
const { onClose }: Props = $props();
let appVersion = $state("");
onMount(async () => {
appVersion = await getVersion();
});
const links = {
source: "https://git.nhcarrigan.com/nhcarrigan/hikari-desktop",
discord: "https://chat.nhcarrigan.com",
website: "https://nhcarrigan.com",
license: "https://docs.nhcarrigan.com/legal/license/",
changelog: "https://git.nhcarrigan.com/nhcarrigan/hikari-desktop/releases",
};
</script>
<div
class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onclick={onClose}
role="button"
tabindex="0"
onkeydown={(e) => e.key === "Escape" && onClose()}
>
<div
class="bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg shadow-xl max-w-md w-full p-6"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
role="dialog"
aria-labelledby="about-title"
tabindex="-1"
>
<div class="flex items-center justify-between mb-4">
<h2 id="about-title" class="text-xl font-semibold text-gray-100">About Hikari Desktop</h2>
<button
onclick={onClose}
class="p-1 text-gray-500 hover:text-gray-300 transition-colors"
aria-label="Close"
>
<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="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div class="space-y-4 text-sm">
<div>
<h3 class="font-medium text-gray-200 mb-2">What is Hikari Desktop?</h3>
<p class="text-gray-400">
Hikari Desktop is an AI-powered desktop assistant that brings Claude directly to your
desktop. Built with love using Tauri, Svelte, and Rust for a fast, native experience.
</p>
</div>
<div>
<h3 class="font-medium text-gray-200 mb-2">Version</h3>
<p class="text-gray-400 mb-1">
{appVersion || "Loading..."}
</p>
<button
onclick={() => openUrl(links.changelog)}
class="text-[var(--accent-primary)] hover:text-[var(--accent-primary-hover)] transition-colors underline"
>
View Changelog
</button>
</div>
<div>
<h3 class="font-medium text-gray-200 mb-2">Source Code</h3>
<button
onclick={() => openUrl(links.source)}
class="text-[var(--accent-primary)] hover:text-[var(--accent-primary-hover)] transition-colors underline"
>
View on Git
</button>
</div>
<div>
<h3 class="font-medium text-gray-200 mb-2">Support & Community</h3>
<p class="text-gray-400 mb-1">Found a bug or have a suggestion?</p>
<button
onclick={() => openUrl(links.discord)}
class="text-[var(--accent-primary)] hover:text-[var(--accent-primary-hover)] transition-colors underline"
>
Join our Discord
</button>
</div>
<div>
<h3 class="font-medium text-gray-200 mb-2">Built with 💕 by</h3>
<button
onclick={() => openUrl(links.website)}
class="text-[var(--accent-primary)] hover:text-[var(--accent-primary-hover)] transition-colors underline"
>
Naomi Carrigan
</button>
</div>
<div>
<h3 class="font-medium text-gray-200 mb-2">License</h3>
<p class="text-gray-400 mb-1">
This project is open source and available under our license terms.
</p>
<button
onclick={() => openUrl(links.license)}
class="text-[var(--accent-primary)] hover:text-[var(--accent-primary-hover)] transition-colors underline"
>
View License
</button>
</div>
<div class="pt-4 mt-4 border-t border-[var(--border-color)]">
<p class="text-xs text-gray-500 text-center">
Copyright © {new Date().getFullYear()} Naomi Carrigan. All rights reserved.
</p>
</div>
</div>
</div>
</div>
<style>
/* Ensure the panel appears above other content */
[role="dialog"] {
animation: slideIn 0.2s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
</style>
+155
View File
@@ -0,0 +1,155 @@
<script lang="ts">
interface Props {
onClose: () => void;
}
const { onClose }: Props = $props();
const sections = [
{
title: "Getting Started",
items: [
"Enter your Claude API key in Settings (gear icon)",
"Set your working directory and click Connect",
"Start chatting with Hikari - your AI assistant!",
],
},
{
title: "Key Features",
items: [
"🗂️ File Management: Hikari can read, write, and edit files in your project",
"💻 Terminal Access: Execute commands and run scripts",
"🔍 Code Search: Find files and search through code",
"🌐 Web Access: Fetch information from the web",
"📊 MCP Servers: Connect to external tools via Model Context Protocol",
"📁 Multi-tab Support: Work on multiple conversations simultaneously",
],
},
{
title: "Available Commands",
items: [
"Type naturally - Hikari understands context!",
"Ask to read, create, or modify files",
"Request code explanations or debugging help",
"Have Hikari run tests or build commands",
"Search for specific functions or patterns",
],
},
{
title: "Tips & Tricks",
items: [
"💡 Use the stats panel to track your usage",
"🎯 Be specific about file paths and requirements",
"🔒 Grant tool permissions as needed for security",
"📌 Pin important conversations for quick access",
"🎨 Customize your theme and preferences in Settings",
],
},
{
title: "Keyboard Shortcuts",
items: [
"Ctrl/Cmd + Enter: Send message",
"Ctrl/Cmd + K: Clear chat (when supported)",
"Escape: Close modals and panels",
],
},
];
</script>
<div
class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onclick={onClose}
role="button"
tabindex="0"
onkeydown={(e) => e.key === "Escape" && onClose()}
>
<div
class="bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg shadow-xl max-w-2xl w-full max-h-[80vh] overflow-hidden flex flex-col"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
role="dialog"
aria-labelledby="help-title"
tabindex="-1"
>
<div class="flex items-center justify-between p-6 pb-4 border-b border-[var(--border-color)]">
<h2 id="help-title" class="text-xl font-semibold text-gray-100">How to Use Hikari Desktop</h2>
<button
onclick={onClose}
class="p-1 text-gray-500 hover:text-gray-300 transition-colors"
aria-label="Close"
>
<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="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div class="overflow-y-auto flex-1 p-6 space-y-6">
{#each sections as section (section.title)}
<div>
<h3 class="font-medium text-gray-200 mb-3">{section.title}</h3>
<ul class="space-y-2 text-sm text-gray-400">
{#each section.items as item (item)}
<li class="flex items-start">
<span class="text-[var(--accent-primary)] mr-2 mt-0.5"></span>
<span>{item}</span>
</li>
{/each}
</ul>
</div>
{/each}
<div class="pt-4 border-t border-[var(--border-color)]">
<p class="text-sm text-gray-500">
<strong>Need more help?</strong> Join our Discord community for support and updates!
</p>
</div>
</div>
</div>
</div>
<style>
/* Ensure the panel appears above other content */
[role="dialog"] {
animation: slideIn 0.2s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: scale(0.95) translateY(10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* Custom scrollbar styling */
.overflow-y-auto {
scrollbar-width: thin;
scrollbar-color: var(--border-color) transparent;
}
.overflow-y-auto::-webkit-scrollbar {
width: 8px;
}
.overflow-y-auto::-webkit-scrollbar-track {
background: transparent;
}
.overflow-y-auto::-webkit-scrollbar-thumb {
background-color: var(--border-color);
border-radius: 4px;
}
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
background-color: var(--accent-primary);
}
</style>
+52
View File
@@ -15,9 +15,12 @@
import type { ConnectionStatus } from "$lib/types/messages";
import { onMount } from "svelte";
import StatsDisplay from "./StatsDisplay.svelte";
import AboutPanel from "./AboutPanel.svelte";
import HelpPanel from "./HelpPanel.svelte";
import { achievementProgress } from "$lib/stores/achievements";
const DISCORD_URL = "https://chat.nhcarrigan.com";
const DONATE_URL = "https://donate.nhcarrigan.com";
let connectionStatus: ConnectionStatus = $state("disconnected");
let workingDirectory = $state("");
@@ -26,6 +29,8 @@
let grantedToolsList: string[] = $state([]);
let appVersion = $state("");
let showStats = $state(false);
let showAbout = $state(false);
let showHelp = $state(false);
const progress = $derived($achievementProgress);
let currentConfig: HikariConfig = $state({
model: null,
@@ -241,6 +246,45 @@
/>
</svg>
</button>
<button
onclick={() => openUrl(DONATE_URL)}
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
title="Support our work"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
/>
</svg>
</button>
<button
onclick={() => (showAbout = true)}
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
title="About Hikari Desktop"
>
<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 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
<button
onclick={() => (showHelp = true)}
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
title="Help"
>
<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="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
<button
onclick={() => openUrl(DISCORD_URL)}
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
@@ -288,3 +332,11 @@
<StatsDisplay />
</div>
{/if}
{#if showAbout}
<AboutPanel onClose={() => (showAbout = false)} />
{/if}
{#if showHelp}
<HelpPanel onClose={() => (showHelp = false)} />
{/if}
+1 -1
View File
@@ -2,7 +2,7 @@ import { NOTIFICATION_SOUNDS, type NotificationType } from "./types";
class SoundPlayer {
private audioCache: Map<NotificationType, HTMLAudioElement> = new Map();
private enabled: boolean = true;
private enabled: boolean = false; // Start disabled until config loads
private globalVolume: number = 1.0;
constructor() {
+16 -15
View File
@@ -1,22 +1,23 @@
import { derived } from "svelte/store";
import { configStore } from "./config";
import { soundPlayer } from "$lib/notifications";
// Sync notification settings with config
export const notificationSettings = derived(configStore.config, ($config) => {
soundPlayer.setEnabled($config.notifications_enabled);
soundPlayer.setGlobalVolume($config.notification_volume);
let unsubscribe: (() => void) | null = null;
return {
enabled: $config.notifications_enabled,
volume: $config.notification_volume,
};
});
// Initialize notification settings sync - call this once on app startup
export function initNotificationSync() {
// Prevent duplicate subscriptions
if (unsubscribe) return;
// Helper to update notification settings
export async function updateNotificationSettings(enabled: boolean, volume: number) {
await configStore.updateConfig({
notifications_enabled: enabled,
notification_volume: volume,
unsubscribe = configStore.config.subscribe(($config) => {
soundPlayer.setEnabled($config.notifications_enabled);
soundPlayer.setGlobalVolume($config.notification_volume);
});
}
// Cleanup function if needed
export function cleanupNotificationSync() {
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
}
}
+5
View File
@@ -2,6 +2,7 @@
import { onMount, onDestroy } from "svelte";
import { initializeTauriListeners, cleanupTauriListeners } from "$lib/tauri";
import { configStore, applyTheme } from "$lib/stores/config";
import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications";
import { conversationsStore } from "$lib/stores/conversations";
import "$lib/notifications/testNotifications";
import Terminal from "$lib/components/Terminal.svelte";
@@ -29,12 +30,16 @@
// Apply saved theme on startup
const config = configStore.getConfig();
applyTheme(config.theme);
// Initialize notification settings sync
initNotificationSync();
}
});
onDestroy(() => {
if (initialized) {
cleanupTauriListeners();
cleanupNotificationSync();
initialized = false;
}
});