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
+137 -6
View File
@@ -1,7 +1,18 @@
import { writable, derived } from "svelte/store";
import { invoke } from "@tauri-apps/api/core";
export type Theme = "dark" | "light";
export type Theme = "dark" | "light" | "high-contrast" | "custom";
export interface CustomThemeColors {
bg_primary: string | null;
bg_secondary: string | null;
bg_terminal: string | null;
accent_primary: string | null;
accent_secondary: string | null;
text_primary: string | null;
text_secondary: string | null;
border_color: string | null;
}
export interface HikariConfig {
model: string | null;
@@ -15,9 +26,17 @@ export interface HikariConfig {
notifications_enabled: boolean;
notification_volume: number;
always_on_top: boolean;
minimize_to_tray: boolean;
update_checks_enabled: boolean;
character_panel_width: number | null;
font_size: number;
streamer_mode: boolean;
streamer_hide_paths: boolean;
compact_mode: boolean;
profile_name: string | null;
profile_avatar_path: string | null;
profile_bio: string | null;
custom_theme_colors: CustomThemeColors;
}
const defaultConfig: HikariConfig = {
@@ -32,9 +51,26 @@ const defaultConfig: HikariConfig = {
notifications_enabled: true,
notification_volume: 0.7,
always_on_top: false,
minimize_to_tray: 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,
},
};
function createConfigStore() {
@@ -90,9 +126,24 @@ function createConfigStore() {
closeSidebar: () => isSidebarOpen.set(false),
toggleSidebar: () => isSidebarOpen.update((open) => !open),
setTheme: async (theme: Theme) => {
await updateConfig({ theme });
applyTheme(theme);
setTheme: async (theme: Theme, customColors?: CustomThemeColors) => {
const updates: Partial<HikariConfig> = { theme };
if (customColors) {
updates.custom_theme_colors = customColors;
}
await updateConfig(updates);
let currentConfig: HikariConfig = defaultConfig;
config.subscribe((c) => (currentConfig = c))();
applyTheme(theme, currentConfig.custom_theme_colors);
},
setCustomThemeColors: async (colors: CustomThemeColors) => {
await updateConfig({ custom_theme_colors: colors });
let currentConfig: HikariConfig = defaultConfig;
config.subscribe((c) => (currentConfig = c))();
if (currentConfig.theme === "custom") {
applyCustomThemeColors(colors);
}
},
setFontSize: async (size: number) => {
@@ -143,15 +194,72 @@ function createConfigStore() {
config.subscribe((c) => (currentConfig = c))();
return currentConfig;
},
toggleStreamerMode: async () => {
let currentConfig: HikariConfig = defaultConfig;
config.subscribe((c) => (currentConfig = c))();
await updateConfig({ streamer_mode: !currentConfig.streamer_mode });
},
toggleCompactMode: async () => {
let currentConfig: HikariConfig = defaultConfig;
config.subscribe((c) => (currentConfig = c))();
await updateConfig({ compact_mode: !currentConfig.compact_mode });
},
setCompactMode: async (enabled: boolean) => {
await updateConfig({ compact_mode: enabled });
},
};
}
export function applyTheme(theme: Theme) {
export function applyTheme(theme: Theme, customColors?: CustomThemeColors) {
if (typeof document !== "undefined") {
document.documentElement.setAttribute("data-theme", theme);
// For custom theme, we use dark as the base and override with custom colors
document.documentElement.setAttribute("data-theme", theme === "custom" ? "dark" : theme);
// Clear any previously applied custom colors
clearCustomThemeColors();
// Apply custom colors if theme is custom
if (theme === "custom" && customColors) {
applyCustomThemeColors(customColors);
}
}
}
export function applyCustomThemeColors(colors: CustomThemeColors) {
if (typeof document === "undefined") return;
const root = document.documentElement;
if (colors.bg_primary) root.style.setProperty("--bg-primary", colors.bg_primary);
if (colors.bg_secondary) root.style.setProperty("--bg-secondary", colors.bg_secondary);
if (colors.bg_terminal) root.style.setProperty("--bg-terminal", colors.bg_terminal);
if (colors.accent_primary) root.style.setProperty("--accent-primary", colors.accent_primary);
if (colors.accent_secondary)
root.style.setProperty("--accent-secondary", colors.accent_secondary);
if (colors.text_primary) root.style.setProperty("--text-primary", colors.text_primary);
if (colors.text_secondary) root.style.setProperty("--text-secondary", colors.text_secondary);
if (colors.border_color) root.style.setProperty("--border-color", colors.border_color);
}
export function clearCustomThemeColors() {
if (typeof document === "undefined") return;
const root = document.documentElement;
const customProperties = [
"--bg-primary",
"--bg-secondary",
"--bg-terminal",
"--accent-primary",
"--accent-secondary",
"--text-primary",
"--text-secondary",
"--border-color",
];
customProperties.forEach((prop) => root.style.removeProperty(prop));
}
const MIN_FONT_SIZE = 10;
const MAX_FONT_SIZE = 24;
const DEFAULT_FONT_SIZE = 14;
@@ -172,3 +280,26 @@ export { MIN_FONT_SIZE, MAX_FONT_SIZE, DEFAULT_FONT_SIZE };
export const configStore = createConfigStore();
export const isDarkTheme = derived(configStore.config, ($config) => $config.theme === "dark");
export const isStreamerMode = derived(configStore.config, ($config) => $config.streamer_mode);
export const isCompactMode = derived(configStore.config, ($config) => $config.compact_mode);
export const shouldHidePaths = derived(
configStore.config,
($config) => $config.streamer_mode && $config.streamer_hide_paths
);
/**
* Masks file paths in text when streamer mode with hide paths is enabled.
* Replaces username portion of paths with asterisks.
*/
export function maskPaths(text: string, hidePaths: boolean): string {
if (!hidePaths) return text;
// Match Unix paths like /home/username/... or /Users/username/...
// and Windows paths like C:\Users\username\...
return text
.replace(/\/home\/([^/\s]+)/g, "/home/****")
.replace(/\/Users\/([^/\s]+)/g, "/Users/****")
.replace(/C:\\Users\\([^\\\s]+)/gi, "C:\\Users\\****")
.replace(/~\//g, "****/");
}