feat: add multiple productivity features and UI enhancements (#68)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 54s
CI / Lint & Test (push) Successful in 14m42s
CI / Build Linux (push) Successful in 19m4s
CI / Build Windows (cross-compile) (push) Successful in 28m37s

## Summary

This PR adds a collection of productivity features and UI enhancements to improve the Hikari Desktop experience:

### New Features
- **Clipboard History** (#25) - Track and manage copied code snippets with language detection, search, filtering, and pinning
- **Quick Actions Panel** (#15) - Buttons for common quick actions like "Review PR", "Run tests", "Explain file", with customizable actions
- **Git Integration Panel** (#24) - View current branch, changed/staged files, quick git actions (commit, push, pull), and branch management
- **Session Import/Export** (#8) - Export conversations to JSON and import previously saved sessions
- **Snippet Library** (#22) - Save and reuse common prompts with categories and quick insert
- **Session History** (#14) - Auto-save conversations with browsable history and search
- **High Contrast Mode** (#20) - Accessibility theme with improved visibility
- **Minimize to System Tray** (#11) - System tray support with right-click menu

### UI Enhancements
- Trans-pride gradient theme applied across UI elements
- Copy button added to code blocks
- Linter formatting and eslint-disable comments for cleaner code

## Closes

Closes #8
Closes #11
Closes #14
Closes #15
Closes #20
Closes #22
Closes #24
Closes #25
Closes #34
Closes #35
Closes #36
Closes #37
Closes #69
Closes #70

## Test Plan

- [ ] Verify clipboard history captures code from code block copy buttons
- [ ] Verify clipboard history captures manually selected text from terminal
- [ ] Test snippet library CRUD operations and insertion
- [ ] Test quick actions panel with default and custom actions
- [ ] Test git panel shows correct status, branch, and performs git operations
- [ ] Test session history auto-save and restore
- [ ] Test session import/export roundtrip
- [ ] Verify high contrast mode provides adequate contrast
- [ ] Test minimize to tray functionality and tray menu
- [ ] Verify trans-pride gradient theme displays correctly in all themes

---
* This PR was created with help from Hikari~ 🌸*

Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Reviewed-on: #68
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #68.
This commit is contained in:
2026-01-25 22:19:00 -08:00
committed by Naomi Carrigan
parent 852a4d6661
commit 4c46d4c8fd
47 changed files with 11695 additions and 319 deletions
+113 -12
View File
@@ -1,9 +1,10 @@
<script lang="ts">
interface Props {
onToggleAchievements?: () => void;
onToggleCompact?: () => void;
}
const { onToggleAchievements = () => {} }: Props = $props();
const { onToggleAchievements = () => {}, onToggleCompact = () => {} }: Props = $props();
import { invoke } from "@tauri-apps/api/core";
import { getVersion } from "@tauri-apps/api/app";
@@ -11,7 +12,7 @@
import { openUrl } from "@tauri-apps/plugin-opener";
import { get } from "svelte/store";
import { claudeStore } from "$lib/stores/claude";
import { configStore, type HikariConfig } from "$lib/stores/config";
import { configStore, type HikariConfig, isStreamerMode } from "$lib/stores/config";
import type { ConnectionStatus } from "$lib/types/messages";
import { onMount } from "svelte";
import StatsDisplay from "./StatsDisplay.svelte";
@@ -19,6 +20,9 @@
import HelpPanel from "./HelpPanel.svelte";
import KeyboardShortcutsModal from "./KeyboardShortcutsModal.svelte";
import { achievementProgress } from "$lib/stores/achievements";
import SessionHistoryPanel from "./SessionHistoryPanel.svelte";
import GitPanel from "./GitPanel.svelte";
import ProfilePanel from "./ProfilePanel.svelte";
const DISCORD_URL = "https://chat.nhcarrigan.com";
const DONATE_URL = "https://donate.nhcarrigan.com";
@@ -33,6 +37,9 @@
let showAbout = $state(false);
let showHelp = $state(false);
let showKeyboardShortcuts = $state(false);
let showSessionHistory = $state(false);
let showGitPanel = $state(false);
let showProfile = $state(false);
const progress = $derived($achievementProgress);
let currentConfig: HikariConfig = $state({
model: null,
@@ -49,6 +56,28 @@
update_checks_enabled: true,
character_panel_width: null,
font_size: 14,
minimize_to_tray: false,
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,
},
});
let streamerModeActive = $state(false);
isStreamerMode.subscribe((value) => {
streamerModeActive = value;
});
onMount(async () => {
@@ -202,9 +231,43 @@
</div>
<div class="flex items-center gap-3">
{#if streamerModeActive}
<div
class="w-2.5 h-2.5 rounded-full bg-red-500 animate-pulse"
title="Streamer mode active (Ctrl+Shift+S to toggle)"
></div>
{/if}
<button
onclick={() => (showProfile = true)}
class="p-1 text-gray-500 icon-trans-hover"
title="Profile"
>
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</button>
<button
onclick={onToggleCompact}
class="p-1 text-gray-500 icon-trans-hover"
title="Compact Mode (Ctrl+Shift+M)"
>
<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="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
/>
</svg>
</button>
<button
onclick={toggleAchievements}
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors relative"
class="p-1 text-gray-500 icon-trans-hover relative"
title="Achievements"
>
<span class="text-lg">🏆</span>
@@ -216,11 +279,37 @@
</span>
{/if}
</button>
<button
onclick={() => (showSessionHistory = true)}
class="p-1 text-gray-500 icon-trans-hover"
title="Session History"
>
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
<button
onclick={() => (showGitPanel = true)}
class="p-1 text-gray-500 icon-trans-hover"
title="Git Panel"
>
<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.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
/>
</svg>
</button>
<button
onclick={() => (showStats = !showStats)}
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors {showStats
? 'text-[var(--accent-primary)]'
: ''}"
class="p-1 text-gray-500 icon-trans-hover {showStats ? 'text-[var(--trans-pink)]' : ''}"
title="Usage Stats"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -234,7 +323,7 @@
</button>
<button
onclick={configStore.openSidebar}
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
class="p-1 text-gray-500 icon-trans-hover"
title="Settings"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -254,7 +343,7 @@
</button>
<button
onclick={() => openUrl(DONATE_URL)}
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
class="p-1 text-gray-500 icon-trans-hover"
title="Support our work"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
@@ -265,7 +354,7 @@
</button>
<button
onclick={() => (showAbout = true)}
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
class="p-1 text-gray-500 icon-trans-hover"
title="About Hikari Desktop"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -279,7 +368,7 @@
</button>
<button
onclick={() => (showKeyboardShortcuts = true)}
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
class="p-1 text-gray-500 icon-trans-hover"
title="Keyboard Shortcuts"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -299,7 +388,7 @@
</button>
<button
onclick={() => (showHelp = true)}
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
class="p-1 text-gray-500 icon-trans-hover"
title="Help"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -313,7 +402,7 @@
</button>
<button
onclick={() => openUrl(DISCORD_URL)}
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
class="p-1 text-gray-500 icon-trans-hover"
title="Join our Discord"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
@@ -370,3 +459,15 @@
{#if showKeyboardShortcuts}
<KeyboardShortcutsModal onClose={() => (showKeyboardShortcuts = false)} />
{/if}
{#if showSessionHistory}
<SessionHistoryPanel onClose={() => (showSessionHistory = false)} />
{/if}
{#if showGitPanel}
<GitPanel isOpen={showGitPanel} onClose={() => (showGitPanel = false)} />
{/if}
{#if showProfile}
<ProfilePanel onClose={() => (showProfile = false)} />
{/if}