generated from nhcarrigan/template
4c46d4c8fd
## 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>
132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
import { writable, derived } from "svelte/store";
|
|
import { listen } from "@tauri-apps/api/event";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
|
|
export interface UsageStats {
|
|
total_input_tokens: number;
|
|
total_output_tokens: number;
|
|
total_cost_usd: number;
|
|
session_input_tokens: number;
|
|
session_output_tokens: number;
|
|
session_cost_usd: number;
|
|
model: string | null;
|
|
|
|
// New fields
|
|
messages_exchanged: number;
|
|
session_messages_exchanged: number;
|
|
code_blocks_generated: number;
|
|
session_code_blocks_generated: number;
|
|
files_edited: number;
|
|
session_files_edited: number;
|
|
files_created: number;
|
|
session_files_created: number;
|
|
tools_usage: Record<string, number>;
|
|
session_tools_usage: Record<string, number>;
|
|
session_duration_seconds: number;
|
|
}
|
|
|
|
// Main stats store
|
|
export const stats = writable<UsageStats>({
|
|
total_input_tokens: 0,
|
|
total_output_tokens: 0,
|
|
total_cost_usd: 0,
|
|
session_input_tokens: 0,
|
|
session_output_tokens: 0,
|
|
session_cost_usd: 0,
|
|
model: null,
|
|
messages_exchanged: 0,
|
|
session_messages_exchanged: 0,
|
|
code_blocks_generated: 0,
|
|
session_code_blocks_generated: 0,
|
|
files_edited: 0,
|
|
session_files_edited: 0,
|
|
files_created: 0,
|
|
session_files_created: 0,
|
|
tools_usage: {},
|
|
session_tools_usage: {},
|
|
session_duration_seconds: 0,
|
|
});
|
|
|
|
// Derived store for formatted display values
|
|
export const formattedStats = derived(stats, ($stats) => {
|
|
const formatNumber = (num: number) => num.toLocaleString();
|
|
const formatCost = (cost: number) => `$${cost.toFixed(4)}`;
|
|
const formatDuration = (seconds: number) => {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
const secs = seconds % 60;
|
|
|
|
if (hours > 0) {
|
|
return `${hours}h ${minutes}m ${secs}s`;
|
|
} else if (minutes > 0) {
|
|
return `${minutes}m ${secs}s`;
|
|
} else {
|
|
return `${secs}s`;
|
|
}
|
|
};
|
|
|
|
return {
|
|
totalTokens: formatNumber($stats.total_input_tokens + $stats.total_output_tokens),
|
|
totalInputTokens: formatNumber($stats.total_input_tokens),
|
|
totalOutputTokens: formatNumber($stats.total_output_tokens),
|
|
totalCost: formatCost($stats.total_cost_usd),
|
|
sessionTokens: formatNumber($stats.session_input_tokens + $stats.session_output_tokens),
|
|
sessionInputTokens: formatNumber($stats.session_input_tokens),
|
|
sessionOutputTokens: formatNumber($stats.session_output_tokens),
|
|
sessionCost: formatCost($stats.session_cost_usd),
|
|
model: $stats.model || "No model selected",
|
|
|
|
// New formatted fields
|
|
messagesTotal: formatNumber($stats.messages_exchanged),
|
|
messagesSession: formatNumber($stats.session_messages_exchanged),
|
|
codeBlocksTotal: formatNumber($stats.code_blocks_generated),
|
|
codeBlocksSession: formatNumber($stats.session_code_blocks_generated),
|
|
filesEditedTotal: formatNumber($stats.files_edited),
|
|
filesEditedSession: formatNumber($stats.session_files_edited),
|
|
filesCreatedTotal: formatNumber($stats.files_created),
|
|
filesCreatedSession: formatNumber($stats.session_files_created),
|
|
sessionDuration: formatDuration($stats.session_duration_seconds),
|
|
toolsUsage: $stats.tools_usage,
|
|
sessionToolsUsage: $stats.session_tools_usage,
|
|
};
|
|
});
|
|
|
|
// Note: Cost calculation is now done in the Rust backend
|
|
|
|
// Initialize stats listener
|
|
export async function initStatsListener() {
|
|
// Listen for stats updates from the backend
|
|
await listen("claude:stats", (event) => {
|
|
const payload = event.payload as { stats: UsageStats };
|
|
const { stats: newStats } = payload;
|
|
|
|
// The backend already tracks all totals - just set the stats directly
|
|
stats.set(newStats);
|
|
});
|
|
|
|
// Load initial persisted stats from backend (no bridge required)
|
|
try {
|
|
const initialStats = await invoke<UsageStats>("get_persisted_stats");
|
|
stats.set(initialStats);
|
|
console.log("Loaded persisted stats:", initialStats);
|
|
} catch (error) {
|
|
console.error("Failed to load initial stats:", error);
|
|
}
|
|
}
|
|
|
|
// Reset session stats (call when starting new session)
|
|
export function resetSessionStats() {
|
|
stats.update((current) => ({
|
|
...current,
|
|
session_input_tokens: 0,
|
|
session_output_tokens: 0,
|
|
session_cost_usd: 0,
|
|
session_messages_exchanged: 0,
|
|
session_code_blocks_generated: 0,
|
|
session_files_edited: 0,
|
|
session_files_created: 0,
|
|
session_tools_usage: {},
|
|
session_duration_seconds: 0,
|
|
}));
|
|
}
|