feat: display friendly names for memory files (closes #177)
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m14s
CI / Lint & Test (pull_request) Successful in 19m13s
CI / Build Linux (pull_request) Successful in 23m5s
CI / Build Windows (cross-compile) (pull_request) Successful in 38m45s

The memory file list now shows human-readable titles instead of raw
filenames wherever possible.

- Rust: adds `MemoryFileInfo { path, heading }` and updates
  `MemoryFilesResponse` to use `Vec<MemoryFileInfo>`; adds
  `extract_first_heading()` helper that scans the first `# Heading`
  from a file's content; both `list_memory_files_native` and
  `list_memory_files_via_wsl` now read each file and populate the
  heading field; raw filename remains available as a fallback
- Frontend: updates `MemoryBrowserPanel.svelte` to use the richer type;
  adds `getDisplayName()` which returns the heading or falls back to the
  raw filename; file buttons use the display name with the raw filename
  as a tooltip; the viewer header also shows the display name with the
  filename shown as a subtitle when a heading is present
- Tests: 8 new Rust unit tests for `extract_first_heading` (happy path,
  edge cases, empty content, whitespace); 12 new TypeScript tests for
  `getFileName` and `getDisplayName` mirrored from the component
This commit is contained in:
2026-03-03 18:19:53 -08:00
committed by Naomi Carrigan
parent 5bfff9d5e0
commit c5feb9b43c
3 changed files with 259 additions and 19 deletions
+32 -10
View File
@@ -3,17 +3,22 @@
import { invoke } from "@tauri-apps/api/core";
import Markdown from "./Markdown.svelte";
let memoryFiles: string[] = $state([]);
interface MemoryFileInfo {
path: string;
heading: string | null;
}
interface MemoryFilesResponse {
files: MemoryFileInfo[];
}
let memoryFiles: MemoryFileInfo[] = $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;
@@ -49,6 +54,10 @@
return path.split("/").pop() || path;
}
function getDisplayName(file: MemoryFileInfo): string {
return file.heading ?? getFileName(file.path);
}
function togglePanel() {
isPanelOpen = !isPanelOpen;
if (isPanelOpen && memoryFiles.length === 0) {
@@ -151,11 +160,12 @@
{:else}
<div class="panel-layout">
<div class="file-list">
{#each memoryFiles as file (file)}
{#each memoryFiles as file (file.path)}
<button
class="file-item"
class:active={selectedFile === file}
onclick={() => loadFileContent(file)}
class:active={selectedFile === file.path}
onclick={() => loadFileContent(file.path)}
title={getFileName(file.path)}
>
<svg
class="file-icon"
@@ -171,7 +181,7 @@
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>
<span class="file-name">{getDisplayName(file)}</span>
</button>
{/each}
</div>
@@ -179,7 +189,12 @@
<div class="file-viewer">
{#if selectedFile && fileContent}
<div class="viewer-header">
<h4>{getFileName(selectedFile)}</h4>
{#each memoryFiles.filter((f) => f.path === selectedFile) as activeFile (activeFile.path)}
<h4>{getDisplayName(activeFile)}</h4>
{#if activeFile.heading}
<p class="viewer-filename">{getFileName(activeFile.path)}</p>
{/if}
{/each}
</div>
<div class="viewer-content">
<Markdown content={fileContent} />
@@ -438,6 +453,13 @@
color: var(--text-primary);
}
.viewer-filename {
margin: 0.25rem 0 0;
font-size: 0.75rem;
color: var(--text-tertiary);
font-family: monospace;
}
.viewer-content {
flex: 1;
padding: 1.5rem;