feat: surface memory file last-modified timestamps in MemoryBrowserPanel (#230)

Extends MemoryFileInfo with last_modified (Unix timestamp) populated via
fs::metadata on native platforms, and displays a formatted date in the
file list as secondary text.
This commit is contained in:
2026-03-20 10:19:27 -07:00
committed by Naomi Carrigan
parent 3f248f7ffa
commit 6945ebc998
2 changed files with 51 additions and 5 deletions
+17 -3
View File
@@ -1464,6 +1464,7 @@ pub async fn close_application(app_handle: AppHandle) -> Result<(), String> {
pub struct MemoryFileInfo {
pub path: String,
pub heading: Option<String>,
pub last_modified: Option<String>, // Unix timestamp in seconds as a string
}
#[derive(serde::Serialize)]
@@ -1535,7 +1536,11 @@ async fn list_memory_files_via_wsl() -> Result<MemoryFilesResponse, String> {
let mut files = Vec::new();
for path in paths {
let heading = read_wsl_file_first_heading(&path);
files.push(MemoryFileInfo { path, heading });
files.push(MemoryFileInfo {
path,
heading,
last_modified: None,
});
}
Ok(MemoryFilesResponse { files })
@@ -1605,14 +1610,23 @@ async fn list_memory_files_native() -> Result<MemoryFilesResponse, String> {
// Sort files alphabetically
memory_paths.sort();
// Read first heading from each file
// Read first heading and modification time from each file
let files = memory_paths
.into_iter()
.map(|path| {
let heading = fs::read_to_string(&path)
.ok()
.and_then(|content| extract_first_heading(&content));
MemoryFileInfo { path, heading }
let last_modified = fs::metadata(&path)
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs().to_string());
MemoryFileInfo {
path,
heading,
last_modified,
}
})
.collect();
+34 -2
View File
@@ -14,6 +14,7 @@
interface MemoryFileInfo {
path: string;
heading: string | null;
last_modified?: string; // Unix timestamp in seconds as a string, optional for backwards compat
}
interface MemoryFilesResponse {
@@ -65,6 +66,16 @@
return file.heading ?? getFileName(file.path);
}
function formatLastModified(ts: string | undefined): string {
if (!ts) return "";
const date = new Date(Number(ts) * 1000);
return date.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
}
async function sendMemoryCommand() {
const conversationId = get(claudeStore.activeConversationId);
if (!conversationId) return;
@@ -205,7 +216,12 @@
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">{getDisplayName(file)}</span>
<div class="file-info">
<span class="file-name">{getDisplayName(file)}</span>
{#if file.last_modified}
<span class="file-date">{formatLastModified(file.last_modified)}</span>
{/if}
</div>
</button>
{/each}
</div>
@@ -416,7 +432,7 @@
.file-item {
display: flex;
align-items: center;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: var(--bg-secondary);
@@ -445,6 +461,13 @@
flex-shrink: 0;
}
.file-info {
display: flex;
flex-direction: column;
gap: 0.125rem;
overflow: hidden;
}
.file-name {
font-size: 0.875rem;
font-weight: 500;
@@ -453,6 +476,15 @@
white-space: nowrap;
}
.file-date {
font-size: 0.75rem;
color: var(--text-secondary);
}
.file-item.active .file-date {
color: rgba(255, 255, 255, 0.75);
}
.file-viewer {
display: flex;
flex-direction: column;