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>
709 lines
21 KiB
TypeScript
709 lines
21 KiB
TypeScript
import { writable } from "svelte/store";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { save, open } from "@tauri-apps/plugin-dialog";
|
|
import type { Conversation } from "./conversations";
|
|
import { writeTextFile, readTextFile } from "@tauri-apps/plugin-fs";
|
|
import { openPath } from "@tauri-apps/plugin-opener";
|
|
|
|
// Debounce delay for auto-save (in milliseconds)
|
|
const AUTO_SAVE_DEBOUNCE_MS = 2000;
|
|
|
|
export interface SavedMessage {
|
|
id: string;
|
|
type: string;
|
|
content: string;
|
|
timestamp: string;
|
|
tool_name?: string;
|
|
}
|
|
|
|
export interface SavedSession {
|
|
id: string;
|
|
name: string;
|
|
created_at: string;
|
|
last_activity_at: string;
|
|
working_directory: string;
|
|
message_count: number;
|
|
preview: string;
|
|
messages: SavedMessage[];
|
|
}
|
|
|
|
export interface SessionListItem {
|
|
id: string;
|
|
name: string;
|
|
created_at: string;
|
|
last_activity_at: string;
|
|
working_directory: string;
|
|
message_count: number;
|
|
preview: string;
|
|
}
|
|
|
|
function escapeHtml(text: string): string {
|
|
return text
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function generateHtmlExport(session: SavedSession, includeMetadata: boolean): string {
|
|
const styles = `
|
|
:root {
|
|
--bg-primary: #1a1a2e;
|
|
--bg-secondary: #16213e;
|
|
--bg-code: #1e1e2e;
|
|
--accent-primary: #e94560;
|
|
--text-primary: #ffffff;
|
|
--text-secondary: #a0a0a0;
|
|
--border-color: #2a2a4a;
|
|
--terminal-user: #22d3ee;
|
|
--terminal-tool: #c084fc;
|
|
--terminal-error: #f87171;
|
|
--hljs-keyword: #f472b6;
|
|
--hljs-string: #a3e635;
|
|
--hljs-number: #fbbf24;
|
|
--hljs-comment: #6b7280;
|
|
--hljs-function: #c084fc;
|
|
--hljs-type: #22d3ee;
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
padding: 2rem;
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
}
|
|
h1 { color: var(--accent-primary); margin-bottom: 1rem; }
|
|
.metadata {
|
|
background: var(--bg-secondary);
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
margin-bottom: 2rem;
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
.metadata p { color: var(--text-secondary); margin: 0.25rem 0; }
|
|
.metadata strong { color: var(--text-primary); }
|
|
hr { border: none; border-top: 1px solid var(--border-color); margin: 1.5rem 0; }
|
|
.message {
|
|
background: var(--bg-secondary);
|
|
border-radius: 8px;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
border-left: 4px solid var(--border-color);
|
|
}
|
|
.message.user { border-left-color: var(--terminal-user); }
|
|
.message.assistant { border-left-color: var(--accent-primary); }
|
|
.message.tool_use { border-left-color: var(--terminal-tool); }
|
|
.message.tool_result { border-left-color: var(--hljs-string); }
|
|
.message.error { border-left-color: var(--terminal-error); }
|
|
.message.system { border-left-color: var(--text-secondary); }
|
|
.message-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 0.5rem;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
.message-type {
|
|
font-weight: bold;
|
|
text-transform: capitalize;
|
|
}
|
|
.message-type.user { color: var(--terminal-user); }
|
|
.message-type.assistant { color: var(--accent-primary); }
|
|
.message-type.tool_use { color: var(--terminal-tool); }
|
|
.message-type.tool_result { color: var(--hljs-string); }
|
|
.message-type.error { color: var(--terminal-error); }
|
|
.message-type.system { color: var(--text-secondary); }
|
|
.timestamp { color: var(--text-secondary); font-size: 0.85rem; }
|
|
.tool-name { color: var(--terminal-tool); font-size: 0.9rem; }
|
|
.message-content {
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
font-family: 'Consolas', 'Monaco', monospace;
|
|
font-size: 0.9rem;
|
|
}
|
|
pre {
|
|
background: var(--bg-code);
|
|
padding: 1rem;
|
|
border-radius: 4px;
|
|
overflow-x: auto;
|
|
margin: 0.5rem 0;
|
|
}
|
|
code {
|
|
font-family: 'Consolas', 'Monaco', monospace;
|
|
font-size: 0.85rem;
|
|
}
|
|
.footer {
|
|
text-align: center;
|
|
color: var(--text-secondary);
|
|
margin-top: 2rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid var(--border-color);
|
|
}
|
|
@media (prefers-color-scheme: light) {
|
|
:root {
|
|
--bg-primary: #f8f9fa;
|
|
--bg-secondary: #ffffff;
|
|
--bg-code: #f5f5f5;
|
|
--text-primary: #1a1a2e;
|
|
--text-secondary: #5a5a7a;
|
|
--border-color: #d0d0e0;
|
|
--terminal-user: #0891b2;
|
|
--terminal-tool: #7c3aed;
|
|
--terminal-error: #dc2626;
|
|
--hljs-keyword: #d946ef;
|
|
--hljs-string: #16a34a;
|
|
}
|
|
}
|
|
`;
|
|
|
|
let messagesHtml = "";
|
|
for (const message of session.messages) {
|
|
const timestamp = new Date(message.timestamp).toLocaleString();
|
|
const typeLabel =
|
|
message.type === "tool_use"
|
|
? "Tool Use"
|
|
: message.type === "tool_result"
|
|
? "Tool Result"
|
|
: message.type.charAt(0).toUpperCase() + message.type.slice(1);
|
|
|
|
messagesHtml += `
|
|
<div class="message ${message.type}">
|
|
<div class="message-header">
|
|
<span class="message-type ${message.type}">${typeLabel}</span>
|
|
<span class="timestamp">${timestamp}</span>
|
|
</div>
|
|
${message.tool_name ? `<div class="tool-name">🔧 ${escapeHtml(message.tool_name)}</div>` : ""}
|
|
<div class="message-content">${escapeHtml(message.content)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
const metadataHtml = includeMetadata
|
|
? `
|
|
<div class="metadata">
|
|
<p><strong>Created:</strong> ${new Date(session.created_at).toLocaleString()}</p>
|
|
<p><strong>Last Activity:</strong> ${new Date(session.last_activity_at).toLocaleString()}</p>
|
|
<p><strong>Working Directory:</strong> ${session.working_directory || "Not set"}</p>
|
|
<p><strong>Messages:</strong> ${session.message_count}</p>
|
|
</div>
|
|
`
|
|
: "";
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${escapeHtml(session.name)} - Hikari Desktop Export</title>
|
|
<style>${styles}</style>
|
|
</head>
|
|
<body>
|
|
<h1>💬 ${escapeHtml(session.name)}</h1>
|
|
${metadataHtml}
|
|
<hr>
|
|
${messagesHtml}
|
|
<div class="footer">
|
|
<p>✨ Exported from Hikari Desktop</p>
|
|
<p>Export date: ${new Date().toLocaleString()}</p>
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
function generatePrintableHtml(session: SavedSession, includeMetadata: boolean): string {
|
|
const printStyles = `
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: 'Georgia', 'Times New Roman', serif;
|
|
font-size: 11pt;
|
|
line-height: 1.5;
|
|
padding: 0.5in;
|
|
max-width: 8.5in;
|
|
margin: 0 auto;
|
|
color: #000;
|
|
background: #fff;
|
|
}
|
|
h1 {
|
|
font-size: 18pt;
|
|
margin-bottom: 0.25in;
|
|
border-bottom: 2px solid #333;
|
|
padding-bottom: 0.1in;
|
|
}
|
|
.metadata {
|
|
background: #f5f5f5;
|
|
padding: 0.15in;
|
|
margin-bottom: 0.25in;
|
|
border: 1px solid #ddd;
|
|
font-size: 9pt;
|
|
}
|
|
.metadata p { margin: 2pt 0; }
|
|
hr { border: none; border-top: 1px solid #ccc; margin: 0.2in 0; }
|
|
.message {
|
|
margin-bottom: 0.15in;
|
|
padding: 0.1in;
|
|
border-left: 3px solid #ccc;
|
|
page-break-inside: avoid;
|
|
}
|
|
.message.user { border-left-color: #0088cc; }
|
|
.message.assistant { border-left-color: #cc0044; }
|
|
.message.tool_use { border-left-color: #8844cc; }
|
|
.message.tool_result { border-left-color: #44cc44; }
|
|
.message.error { border-left-color: #cc4444; }
|
|
.message.system { border-left-color: #888; }
|
|
.message-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 9pt;
|
|
color: #666;
|
|
margin-bottom: 0.05in;
|
|
border-bottom: 1px dotted #ddd;
|
|
padding-bottom: 0.03in;
|
|
}
|
|
.message-type { font-weight: bold; text-transform: uppercase; }
|
|
.message-type.user { color: #0066aa; }
|
|
.message-type.assistant { color: #aa0033; }
|
|
.message-type.tool_use { color: #6633aa; }
|
|
.message-type.tool_result { color: #33aa33; }
|
|
.message-type.error { color: #aa3333; }
|
|
.tool-name { color: #666; font-size: 9pt; font-style: italic; }
|
|
.message-content {
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
font-family: 'Consolas', 'Courier New', monospace;
|
|
font-size: 9pt;
|
|
background: #fafafa;
|
|
padding: 0.05in;
|
|
}
|
|
.footer {
|
|
text-align: center;
|
|
font-size: 8pt;
|
|
color: #999;
|
|
margin-top: 0.25in;
|
|
padding-top: 0.1in;
|
|
border-top: 1px solid #ddd;
|
|
}
|
|
.print-instructions {
|
|
background: #ffffcc;
|
|
border: 1px solid #cccc00;
|
|
padding: 0.15in;
|
|
margin-bottom: 0.25in;
|
|
font-size: 10pt;
|
|
}
|
|
.print-instructions h2 { font-size: 12pt; margin-bottom: 0.1in; }
|
|
.print-instructions ol { margin-left: 0.2in; }
|
|
.print-instructions li { margin: 0.05in 0; }
|
|
@media print {
|
|
.print-instructions { display: none; }
|
|
body { padding: 0; }
|
|
}
|
|
@page { margin: 0.5in; }
|
|
`;
|
|
|
|
let messagesHtml = "";
|
|
for (const message of session.messages) {
|
|
const timestamp = new Date(message.timestamp).toLocaleString();
|
|
const typeLabel =
|
|
message.type === "tool_use"
|
|
? "Tool"
|
|
: message.type === "tool_result"
|
|
? "Result"
|
|
: message.type.charAt(0).toUpperCase() + message.type.slice(1);
|
|
|
|
messagesHtml += `
|
|
<div class="message ${message.type}">
|
|
<div class="message-header">
|
|
<span class="message-type ${message.type}">${typeLabel}</span>
|
|
<span class="timestamp">${timestamp}</span>
|
|
</div>
|
|
${message.tool_name ? `<div class="tool-name">${escapeHtml(message.tool_name)}</div>` : ""}
|
|
<div class="message-content">${escapeHtml(message.content)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
const metadataHtml = includeMetadata
|
|
? `
|
|
<div class="metadata">
|
|
<p><strong>Created:</strong> ${new Date(session.created_at).toLocaleString()}</p>
|
|
<p><strong>Last Activity:</strong> ${new Date(session.last_activity_at).toLocaleString()}</p>
|
|
<p><strong>Working Directory:</strong> ${session.working_directory || "Not set"}</p>
|
|
<p><strong>Messages:</strong> ${session.message_count}</p>
|
|
</div>
|
|
`
|
|
: "";
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${escapeHtml(session.name)} - Print to PDF</title>
|
|
<style>${printStyles}</style>
|
|
</head>
|
|
<body>
|
|
<div class="print-instructions">
|
|
<h2>Print to PDF Instructions</h2>
|
|
<ol>
|
|
<li>Press <strong>Ctrl+P</strong> (or <strong>Cmd+P</strong> on Mac) to open the print dialog</li>
|
|
<li>Select <strong>"Save as PDF"</strong> or <strong>"Microsoft Print to PDF"</strong> as the printer</li>
|
|
<li>Click <strong>Print</strong> to save your PDF</li>
|
|
</ol>
|
|
<p><em>This instruction box will not appear in your PDF.</em></p>
|
|
</div>
|
|
<h1>${escapeHtml(session.name)}</h1>
|
|
${metadataHtml}
|
|
<hr>
|
|
${messagesHtml}
|
|
<div class="footer">
|
|
<p>Exported from Hikari Desktop | ${new Date().toLocaleString()}</p>
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
function createSessionsStore() {
|
|
const sessions = writable<SessionListItem[]>([]);
|
|
const isLoading = writable(false);
|
|
const searchQuery = writable("");
|
|
|
|
// Track pending auto-save timeouts per conversation
|
|
const pendingAutoSaves = new Map<string, ReturnType<typeof setTimeout>>();
|
|
|
|
async function loadSessions(): Promise<void> {
|
|
isLoading.set(true);
|
|
try {
|
|
const result = await invoke<SessionListItem[]>("list_sessions");
|
|
sessions.set(result);
|
|
} catch (error) {
|
|
console.error("Failed to load sessions:", error);
|
|
} finally {
|
|
isLoading.set(false);
|
|
}
|
|
}
|
|
|
|
async function saveConversation(conversation: Conversation): Promise<void> {
|
|
const messages: SavedMessage[] = conversation.terminalLines.map((line) => ({
|
|
id: line.id,
|
|
type: line.type,
|
|
content: line.content,
|
|
timestamp: line.timestamp.toISOString(),
|
|
tool_name: line.toolName,
|
|
}));
|
|
|
|
const userAndAssistantMessages = conversation.terminalLines.filter(
|
|
(line) => line.type === "user" || line.type === "assistant"
|
|
);
|
|
const previewContent =
|
|
userAndAssistantMessages
|
|
.slice(0, 3)
|
|
.map((m) => m.content)
|
|
.join(" ")
|
|
.slice(0, 150) + (userAndAssistantMessages.length > 3 ? "..." : "");
|
|
|
|
const session: SavedSession = {
|
|
id: conversation.id,
|
|
name: conversation.name,
|
|
created_at: conversation.createdAt.toISOString(),
|
|
last_activity_at: conversation.lastActivityAt.toISOString(),
|
|
working_directory: conversation.workingDirectory,
|
|
message_count: conversation.terminalLines.length,
|
|
preview: previewContent || "Empty conversation",
|
|
messages,
|
|
};
|
|
|
|
try {
|
|
await invoke("save_session", { session });
|
|
await loadSessions();
|
|
} catch (error) {
|
|
console.error("Failed to save session:", error);
|
|
}
|
|
}
|
|
|
|
async function loadSession(sessionId: string): Promise<SavedSession | null> {
|
|
try {
|
|
const session = await invoke<SavedSession | null>("load_session", {
|
|
sessionId,
|
|
});
|
|
return session;
|
|
} catch (error) {
|
|
console.error("Failed to load session:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function deleteSession(sessionId: string): Promise<void> {
|
|
try {
|
|
await invoke("delete_session", { sessionId });
|
|
await loadSessions();
|
|
} catch (error) {
|
|
console.error("Failed to delete session:", error);
|
|
}
|
|
}
|
|
|
|
async function searchSessions(query: string): Promise<void> {
|
|
searchQuery.set(query);
|
|
|
|
if (!query.trim()) {
|
|
await loadSessions();
|
|
return;
|
|
}
|
|
|
|
isLoading.set(true);
|
|
try {
|
|
const result = await invoke<SessionListItem[]>("search_sessions", {
|
|
query,
|
|
});
|
|
sessions.set(result);
|
|
} catch (error) {
|
|
console.error("Failed to search sessions:", error);
|
|
} finally {
|
|
isLoading.set(false);
|
|
}
|
|
}
|
|
|
|
async function clearAllSessions(): Promise<void> {
|
|
try {
|
|
await invoke("clear_all_sessions");
|
|
sessions.set([]);
|
|
} catch (error) {
|
|
console.error("Failed to clear sessions:", error);
|
|
}
|
|
}
|
|
|
|
function scheduleAutoSave(conversation: Conversation): void {
|
|
// Don't auto-save empty conversations
|
|
if (conversation.terminalLines.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Clear any pending auto-save for this conversation
|
|
const existingTimeout = pendingAutoSaves.get(conversation.id);
|
|
if (existingTimeout) {
|
|
clearTimeout(existingTimeout);
|
|
}
|
|
|
|
// Schedule a new auto-save
|
|
const timeout = setTimeout(async () => {
|
|
pendingAutoSaves.delete(conversation.id);
|
|
try {
|
|
await saveConversation(conversation);
|
|
} catch (error) {
|
|
console.error("Auto-save failed:", error);
|
|
}
|
|
}, AUTO_SAVE_DEBOUNCE_MS);
|
|
|
|
pendingAutoSaves.set(conversation.id, timeout);
|
|
}
|
|
|
|
function cancelAutoSave(conversationId: string): void {
|
|
const existingTimeout = pendingAutoSaves.get(conversationId);
|
|
if (existingTimeout) {
|
|
clearTimeout(existingTimeout);
|
|
pendingAutoSaves.delete(conversationId);
|
|
}
|
|
}
|
|
|
|
async function exportSessionAsJson(sessionId: string): Promise<boolean> {
|
|
try {
|
|
const session = await loadSession(sessionId);
|
|
if (!session) {
|
|
console.error("Session not found for export");
|
|
return false;
|
|
}
|
|
|
|
const filePath = await save({
|
|
defaultPath: `hikari-session-${session.name.replace(/[^a-zA-Z0-9]/g, "-")}.json`,
|
|
filters: [{ name: "JSON", extensions: ["json"] }],
|
|
});
|
|
|
|
if (!filePath) {
|
|
return false;
|
|
}
|
|
|
|
const exportData = {
|
|
version: 1,
|
|
exported_at: new Date().toISOString(),
|
|
session,
|
|
};
|
|
|
|
await writeTextFile(filePath, JSON.stringify(exportData, null, 2));
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Failed to export session as JSON:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function exportSessionAsMarkdown(sessionId: string): Promise<boolean> {
|
|
try {
|
|
const session = await loadSession(sessionId);
|
|
if (!session) {
|
|
console.error("Session not found for export");
|
|
return false;
|
|
}
|
|
|
|
const filePath = await save({
|
|
defaultPath: `hikari-session-${session.name.replace(/[^a-zA-Z0-9]/g, "-")}.md`,
|
|
filters: [{ name: "Markdown", extensions: ["md"] }],
|
|
});
|
|
|
|
if (!filePath) {
|
|
return false;
|
|
}
|
|
|
|
let markdown = `# ${session.name}\n\n`;
|
|
markdown += `**Created:** ${new Date(session.created_at).toLocaleString()}\n`;
|
|
markdown += `**Last Activity:** ${new Date(session.last_activity_at).toLocaleString()}\n`;
|
|
markdown += `**Working Directory:** ${session.working_directory || "Not set"}\n`;
|
|
markdown += `**Messages:** ${session.message_count}\n\n`;
|
|
markdown += `---\n\n`;
|
|
|
|
for (const message of session.messages) {
|
|
const timestamp = new Date(message.timestamp).toLocaleTimeString();
|
|
if (message.type === "user") {
|
|
markdown += `## 👤 User (${timestamp})\n\n${message.content}\n\n`;
|
|
} else if (message.type === "assistant") {
|
|
markdown += `## 🤖 Assistant (${timestamp})\n\n${message.content}\n\n`;
|
|
} else if (message.type === "tool_use") {
|
|
markdown += `### 🔧 Tool: ${message.tool_name || "Unknown"} (${timestamp})\n\n\`\`\`\n${message.content}\n\`\`\`\n\n`;
|
|
} else if (message.type === "tool_result") {
|
|
markdown += `### 📋 Result (${timestamp})\n\n\`\`\`\n${message.content}\n\`\`\`\n\n`;
|
|
} else if (message.type === "system") {
|
|
markdown += `> **System:** ${message.content}\n\n`;
|
|
} else if (message.type === "error") {
|
|
markdown += `> ⚠️ **Error:** ${message.content}\n\n`;
|
|
}
|
|
}
|
|
|
|
markdown += `---\n\n*Exported from Hikari Desktop*\n`;
|
|
|
|
await writeTextFile(filePath, markdown);
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Failed to export session as Markdown:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function exportSessionAsHtml(
|
|
sessionId: string,
|
|
includeMetadata: boolean = true
|
|
): Promise<boolean> {
|
|
try {
|
|
const session = await loadSession(sessionId);
|
|
if (!session) {
|
|
console.error("Session not found for export");
|
|
return false;
|
|
}
|
|
|
|
const filePath = await save({
|
|
defaultPath: `hikari-session-${session.name.replace(/[^a-zA-Z0-9]/g, "-")}.html`,
|
|
filters: [{ name: "HTML", extensions: ["html"] }],
|
|
});
|
|
|
|
if (!filePath) {
|
|
return false;
|
|
}
|
|
|
|
const html = generateHtmlExport(session, includeMetadata);
|
|
await writeTextFile(filePath, html);
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Failed to export session as HTML:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function exportSessionAsPdf(
|
|
sessionId: string,
|
|
includeMetadata: boolean = true
|
|
): Promise<boolean> {
|
|
try {
|
|
const session = await loadSession(sessionId);
|
|
if (!session) {
|
|
console.error("Session not found for export");
|
|
return false;
|
|
}
|
|
|
|
const filePath = await save({
|
|
defaultPath: `hikari-session-${session.name.replace(/[^a-zA-Z0-9]/g, "-")}-print.html`,
|
|
filters: [{ name: "HTML (for PDF printing)", extensions: ["html"] }],
|
|
});
|
|
|
|
if (!filePath) {
|
|
return false;
|
|
}
|
|
|
|
const html = generatePrintableHtml(session, includeMetadata);
|
|
await writeTextFile(filePath, html);
|
|
|
|
// Open the file in the default browser for print-to-PDF
|
|
await openPath(filePath);
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Failed to export session for PDF:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function importSession(): Promise<boolean> {
|
|
try {
|
|
const filePath = await open({
|
|
filters: [{ name: "JSON", extensions: ["json"] }],
|
|
multiple: false,
|
|
});
|
|
|
|
if (!filePath) {
|
|
return false;
|
|
}
|
|
|
|
const content = await readTextFile(filePath as string);
|
|
const importData = JSON.parse(content);
|
|
|
|
if (!importData.session || !importData.session.id) {
|
|
console.error("Invalid session file format");
|
|
return false;
|
|
}
|
|
|
|
const session: SavedSession = importData.session;
|
|
|
|
// Generate a new ID to avoid conflicts with existing sessions
|
|
session.id = `imported-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
session.name = `${session.name} (Imported)`;
|
|
|
|
await invoke("save_session", { session });
|
|
await loadSessions();
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Failed to import session:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return {
|
|
sessions: { subscribe: sessions.subscribe },
|
|
isLoading: { subscribe: isLoading.subscribe },
|
|
searchQuery: { subscribe: searchQuery.subscribe },
|
|
loadSessions,
|
|
saveConversation,
|
|
loadSession,
|
|
deleteSession,
|
|
searchSessions,
|
|
clearAllSessions,
|
|
scheduleAutoSave,
|
|
cancelAutoSave,
|
|
exportSessionAsJson,
|
|
exportSessionAsMarkdown,
|
|
exportSessionAsHtml,
|
|
exportSessionAsPdf,
|
|
importSession,
|
|
};
|
|
}
|
|
|
|
export const sessionsStore = createSessionsStore();
|