generated from nhcarrigan/template
778e016bf5
## Summary Fixes the memory files tab showing as empty on Windows production builds and the "forbidden path" error when trying to read memory files. ## Changes ### 1. List memory files from WSL home directory (commit 1) - Split `list_memory_files()` into platform-specific implementations - **Windows**: Use WSL command with `bash -l` to find memory files in WSL home (`~/.claude/projects/.../memory/`) - **Linux/Mac**: Continue using native filesystem access - Previously used `dirs::home_dir()` which returns Windows home (`C:\Users\...`), but Claude Code stores files in WSL home ### 2. Use backend command for reading files (commit 2) - Changed frontend from Tauri's `readTextFile` plugin to `read_file_content` backend command - Tauri plugin enforces scope restrictions and can't access WSL paths on Windows - Our backend command already handles WSL paths correctly via `read_file_via_wsl()` - Matches the pattern used throughout the app for other file operations ## Testing - ✅ All 426 backend tests pass - ✅ All frontend tests pass - ✅ Lint, format, and type checks pass - ✅ Follows existing WSL file operation patterns in codebase ## Related Issues Fixes the memory files tab functionality on Windows whilst maintaining full compatibility with Linux/Mac. ✨ This PR was created by Hikari~ 🌸 Reviewed-on: #140 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
459 lines
11 KiB
Svelte
459 lines
11 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import Markdown from "./Markdown.svelte";
|
|
|
|
let memoryFiles: string[] = $state([]);
|
|
let selectedFile: string | null = $state(null);
|
|
let fileContent: string = $state("");
|
|
let isLoading = $state(false);
|
|
let error: string | null = $state(null);
|
|
let isPanelOpen = $state(false);
|
|
|
|
interface MemoryFilesResponse {
|
|
files: string[];
|
|
}
|
|
|
|
async function loadMemoryFiles() {
|
|
isLoading = true;
|
|
error = null;
|
|
try {
|
|
const response = await invoke<MemoryFilesResponse>("list_memory_files");
|
|
memoryFiles = response.files;
|
|
} catch (e) {
|
|
error = `Failed to load memory files: ${e}`;
|
|
console.error(error);
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
async function loadFileContent(filePath: string) {
|
|
isLoading = true;
|
|
error = null;
|
|
try {
|
|
// Use our backend command instead of Tauri plugin to handle WSL paths
|
|
const content = await invoke<string>("read_file_content", { path: filePath });
|
|
fileContent = content;
|
|
selectedFile = filePath;
|
|
} catch (e) {
|
|
error = `Failed to read file: ${e}`;
|
|
console.error(error);
|
|
fileContent = "";
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
function getFileName(path: string): string {
|
|
return path.split("/").pop() || path;
|
|
}
|
|
|
|
function togglePanel() {
|
|
isPanelOpen = !isPanelOpen;
|
|
if (isPanelOpen && memoryFiles.length === 0) {
|
|
loadMemoryFiles();
|
|
}
|
|
}
|
|
|
|
onMount(() => {
|
|
// Don't load on mount - only when panel is opened
|
|
});
|
|
</script>
|
|
|
|
<button class="memory-toggle" onclick={togglePanel} title="Memory Browser">
|
|
<svg
|
|
class="icon"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
|
|
/>
|
|
</svg>
|
|
<span class="label">Memory</span>
|
|
</button>
|
|
|
|
{#if isPanelOpen}
|
|
<div class="memory-panel">
|
|
<div class="panel-header">
|
|
<div class="header-title">
|
|
<svg
|
|
class="header-icon"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
|
|
/>
|
|
</svg>
|
|
<h3>Memory Files</h3>
|
|
</div>
|
|
<button class="close-btn" onclick={togglePanel} title="Close">
|
|
<svg
|
|
class="close-icon"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="panel-content">
|
|
{#if isLoading && memoryFiles.length === 0}
|
|
<div class="loading">
|
|
<svg
|
|
class="spinner"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
/>
|
|
</svg>
|
|
Loading memory files...
|
|
</div>
|
|
{:else if error}
|
|
<div class="error">
|
|
<p>{error}</p>
|
|
<button class="retry-btn" onclick={loadMemoryFiles}>Retry</button>
|
|
</div>
|
|
{:else if memoryFiles.length === 0}
|
|
<div class="empty">
|
|
<p>No memory files found.</p>
|
|
<p class="hint">
|
|
Memory files are created automatically as I learn from our conversations!
|
|
</p>
|
|
</div>
|
|
{:else}
|
|
<div class="panel-layout">
|
|
<div class="file-list">
|
|
{#each memoryFiles as file (file)}
|
|
<button
|
|
class="file-item"
|
|
class:active={selectedFile === file}
|
|
onclick={() => loadFileContent(file)}
|
|
>
|
|
<svg
|
|
class="file-icon"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
/>
|
|
</svg>
|
|
<span class="file-name">{getFileName(file)}</span>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
|
|
<div class="file-viewer">
|
|
{#if selectedFile && fileContent}
|
|
<div class="viewer-header">
|
|
<h4>{getFileName(selectedFile)}</h4>
|
|
</div>
|
|
<div class="viewer-content">
|
|
<Markdown content={fileContent} />
|
|
</div>
|
|
{:else if selectedFile && isLoading}
|
|
<div class="loading-file">
|
|
<svg
|
|
class="spinner"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
/>
|
|
</svg>
|
|
Loading file...
|
|
</div>
|
|
{:else}
|
|
<div class="no-selection">
|
|
<p>Select a memory file to view its contents</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.memory-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem 1rem;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
color: var(--text-primary);
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.memory-toggle:hover {
|
|
background: var(--bg-hover);
|
|
border-color: var(--accent-primary);
|
|
}
|
|
|
|
.icon {
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
}
|
|
|
|
.label {
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.memory-panel {
|
|
position: fixed;
|
|
top: 0;
|
|
right: 0;
|
|
width: 600px;
|
|
height: 100vh;
|
|
background: var(--bg-primary);
|
|
border-left: 1px solid var(--border-color);
|
|
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.3);
|
|
z-index: 1000;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.panel-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 1rem 1.5rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
background: var(--bg-secondary);
|
|
}
|
|
|
|
.header-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.header-icon {
|
|
width: 1.5rem;
|
|
height: 1.5rem;
|
|
color: var(--accent-primary);
|
|
}
|
|
|
|
.panel-header h3 {
|
|
margin: 0;
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.close-btn {
|
|
padding: 0.5rem;
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.close-btn:hover {
|
|
background: var(--bg-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.close-icon {
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
}
|
|
|
|
.panel-content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.loading,
|
|
.error,
|
|
.empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 1rem;
|
|
padding: 3rem 1.5rem;
|
|
text-align: center;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.spinner {
|
|
width: 2.5rem;
|
|
height: 2.5rem;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
from {
|
|
transform: rotate(0deg);
|
|
}
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.error p {
|
|
color: var(--terminal-error, #f87171);
|
|
}
|
|
|
|
.retry-btn {
|
|
padding: 0.5rem 1rem;
|
|
background: var(--accent-primary);
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: white;
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.retry-btn:hover {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.hint {
|
|
font-size: 0.875rem;
|
|
font-style: italic;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.panel-layout {
|
|
display: grid;
|
|
grid-template-columns: 200px 1fr;
|
|
gap: 1.5rem;
|
|
height: 100%;
|
|
}
|
|
|
|
.file-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.file-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem 1rem;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
color: var(--text-primary);
|
|
cursor: pointer;
|
|
text-align: left;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.file-item:hover {
|
|
background: var(--bg-hover);
|
|
border-color: var(--accent-primary);
|
|
}
|
|
|
|
.file-item.active {
|
|
background: var(--accent-primary);
|
|
border-color: var(--accent-primary);
|
|
color: white;
|
|
}
|
|
|
|
.file-icon {
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.file-name {
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.file-viewer {
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.viewer-header {
|
|
padding: 1rem 1.5rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
background: var(--bg-primary);
|
|
}
|
|
|
|
.viewer-header h4 {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.viewer-content {
|
|
flex: 1;
|
|
padding: 1.5rem;
|
|
overflow-y: auto;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.loading-file,
|
|
.no-selection {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 1rem;
|
|
padding: 3rem 1.5rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
</style>
|