Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src-tauri/src/commands/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ pub fn read_file(path: String) -> Result<String, String> {
fs::read_to_string(file_path).map_err(|e| format!("Failed to read file '{}': {}", path, e))
}

/// Tauri command: Reads a file, returning at most `max_bytes` from the end.
///
/// For large files (e.g., multi-MB JSONL transcripts), this avoids sending
/// the full content through Tauri's JSON-based IPC which would freeze the UI.
///
/// Tauri command: Writes content to a file at the given path.
/// Creates the file if it doesn't exist, overwrites if it does.
/// Uses atomic write (write to temp then rename) to prevent data loss.
Expand Down
47 changes: 42 additions & 5 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use commands::session::{load_session_state, save_session_state};
use std::path::PathBuf;
use std::sync::Mutex;
use tauri::menu::{CheckMenuItemBuilder, MenuBuilder, MenuItemBuilder, SubmenuBuilder};
use tauri::{Emitter, Manager, RunEvent};
use tauri::{Emitter, Listener, Manager, RunEvent};

/// Supported markdown/text file extensions for file-open events.
const SUPPORTED_EXTENSIONS: &[&str] = &["md", "markdown", "mdown", "mkd", "mdwn", "mdtxt", "txt"];
Expand Down Expand Up @@ -277,18 +277,28 @@ pub fn run() {
.quit()
.build()?;

let theme_light = CheckMenuItemBuilder::with_id("theme-light", "Light").build(app)?;
let theme_dark = CheckMenuItemBuilder::with_id("theme-dark", "Dark").build(app)?;
let theme_light = CheckMenuItemBuilder::with_id("theme-light", "Light")
.checked(false)
.build(app)?;
let theme_dark = CheckMenuItemBuilder::with_id("theme-dark", "Dark")
.checked(false)
.build(app)?;
let theme_system =
CheckMenuItemBuilder::with_id("theme-system", "System (Auto)").build(app)?;
CheckMenuItemBuilder::with_id("theme-system", "System (Auto)")
.checked(false)
.build(app)?;
let theme_solarized_light =
CheckMenuItemBuilder::with_id("theme-solarized-light", "Solarized Light")
.checked(false)
.build(app)?;
let theme_solarized_dark =
CheckMenuItemBuilder::with_id("theme-solarized-dark", "Solarized Dark")
.checked(false)
.build(app)?;
let theme_github =
CheckMenuItemBuilder::with_id("theme-github", "GitHub").build(app)?;
CheckMenuItemBuilder::with_id("theme-github", "GitHub")
.checked(false)
.build(app)?;

let themes_menu = SubmenuBuilder::new(app, "Themes")
.item(&theme_light)
Expand Down Expand Up @@ -350,6 +360,29 @@ pub fn run() {

app.set_menu(menu)?;

// Clone theme items for use in event handlers
let theme_items: Vec<tauri::menu::CheckMenuItem<tauri::Wry>> = vec![
theme_light.clone(),
theme_dark.clone(),
theme_system.clone(),
theme_solarized_light.clone(),
theme_solarized_dark.clone(),
theme_github.clone(),
];
let theme_items_sync = theme_items.clone();

// Listen for frontend theme sync (initial load + preferences UI changes)
app.listen("sync-theme-menu", move |event: tauri::Event| {
let payload = event.payload();
// Payload arrives as JSON string e.g. "\"theme-system\""
let selected_id = payload
.trim_matches('"')
.trim();
for item in &theme_items_sync {
let _ = item.set_checked(item.id().0.as_str() == selected_id);
}
});

app.on_menu_event(move |app_handle, event| {
let id = event.id().0.as_str();
match id {
Expand Down Expand Up @@ -454,6 +487,10 @@ pub fn run() {
"system" => "auto",
other => other,
};
// Update checkmarks: uncheck all, check selected
for item in &theme_items {
let _ = item.set_checked(item.id().0.as_str() == id);
}
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.emit("menu-set-theme", theme_value);
}
Expand Down
20 changes: 16 additions & 4 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
<Sidebar />
<main class="editor-area">
<div v-if="tabsStore.activeTab" class="editor-content">
<TranscriptViewer v-if="tabsStore.activeTab.fileType === 'transcript'" />
<Editor v-else-if="editorModeStore.mode === 'wysiwyg'" ref="editorRef" />
<Editor v-if="editorModeStore.mode === 'wysiwyg'" ref="editorRef" />
<SourceEditor v-else />
</div>
<div v-else class="no-tab-placeholder">
Expand Down Expand Up @@ -41,11 +40,10 @@

<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import { listen, emit, type UnlistenFn } from '@tauri-apps/api/event'
import { invoke } from '@tauri-apps/api/core'
import TabBar from './components/tabs/TabBar.vue'
import Editor from './components/Editor.vue'
import TranscriptViewer from './components/transcript/TranscriptViewer.vue'
import SourceEditor from './components/source/SourceEditor.vue'
import Sidebar from './components/sidebar/Sidebar.vue'
import OutlinePanel from './components/sidebar/OutlinePanel.vue'
Expand Down Expand Up @@ -436,6 +434,11 @@ onMounted(async () => {
// Initialize preferences (apply theme, editor styles)
preferencesStore.initialize()

// Sync theme checkmark in native menu
const themeMenuId =
preferencesStore.theme === 'auto' ? 'theme-system' : `theme-${preferencesStore.theme}`
emit('sync-theme-menu', themeMenuId).catch(() => {})

// Start periodic conflict detection for external file changes
autoSaveStore.startConflictDetection()

Expand All @@ -450,6 +453,15 @@ onMounted(async () => {
}
})

// Sync native theme menu checkmark when theme changes (e.g. from Preferences UI)
watch(
() => preferencesStore.theme,
(newTheme) => {
const menuId = newTheme === 'auto' ? 'theme-system' : `theme-${newTheme}`
emit('sync-theme-menu', menuId).catch(() => {})
},
)

// Watch for active tab changes — sync auto-save status and cancel pending saves
watch(
() => tabsStore.activeTabId,
Expand Down
17 changes: 0 additions & 17 deletions src/__tests__/components/sidebar/AiFilesPanel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,6 @@ describe('AiFilesPanel', () => {
expect(rows).toHaveLength(2)
})

it('renders sessions section', () => {
const aiFiles = useAiFilesStore()
aiFiles.sessions = [
{
name: 'session-abc.json',
path: '/dir/session-abc.json',
category: 'session',
modifiedAt: Math.floor(Date.now() / 1000) - 3600,
},
]

const wrapper = mount(AiFilesPanel)

expect(wrapper.text()).toContain('Sessions')
expect(wrapper.text()).toContain('session-abc.json')
})

it('renders memory section', () => {
const aiFiles = useAiFilesStore()
aiFiles.memoryFiles = [
Expand Down
32 changes: 6 additions & 26 deletions src/__tests__/stores/aiFiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ describe('useAiFilesStore', () => {
expect(store.error).toBeNull()
expect(store.claudeProjectPath).toBeNull()
expect(store.instructions).toEqual([])
expect(store.sessions).toEqual([])
expect(store.memoryFiles).toEqual([])
})

Expand All @@ -42,16 +41,6 @@ describe('useAiFilesStore', () => {
expect(store.hasAnyFiles).toBe(true)
})

it('returns true when sessions exist', () => {
const store = useAiFilesStore()

store.sessions = [
{ name: 'session.json', path: '/dir/session.json', category: 'session', modifiedAt: 1000 },
]

expect(store.hasAnyFiles).toBe(true)
})

it('returns true when memory files exist', () => {
const store = useAiFilesStore()

Expand All @@ -73,9 +62,6 @@ describe('useAiFilesStore', () => {
store.instructions = [
{ name: 'CLAUDE.md', path: '/project/CLAUDE.md', category: 'instruction' },
]
store.sessions = [
{ name: 'session.json', path: '/dir/session.json', category: 'session', modifiedAt: 1000 },
]
store.memoryFiles = [
{ name: 'MEMORY.md', path: '/dir/MEMORY.md', category: 'memory', modifiedAt: 1000 },
]
Expand All @@ -86,7 +72,6 @@ describe('useAiFilesStore', () => {
expect(store.error).toBeNull()
expect(store.claudeProjectPath).toBeNull()
expect(store.instructions).toEqual([])
expect(store.sessions).toEqual([])
expect(store.memoryFiles).toEqual([])
})
})
Expand All @@ -110,7 +95,7 @@ describe('useAiFilesStore', () => {
expect(store.instructions[0]!.category).toBe('instruction')
})

it('populates sessions and memory when claude project dir exists', async () => {
it('populates memory files when claude project dir exists', async () => {
const store = useAiFilesStore()

mockInvoke.mockImplementation((cmd: string) => {
Expand All @@ -119,20 +104,16 @@ describe('useAiFilesStore', () => {
if (cmd === 'find_instruction_files') return Promise.resolve([])
if (cmd === 'list_files_with_mtime')
return Promise.resolve([
{ name: 'session.json', path: '/dir/session.json', modified_at: 2000 },
{ name: 'MEMORY.md', path: '/dir/MEMORY.md', modified_at: 1000 },
{ name: 'data.jsonl', path: '/dir/data.jsonl', modified_at: 3000 },
{ name: 'notes.md', path: '/dir/notes.md', modified_at: 2000 },
])
return Promise.resolve([])
})

await store.discoverFiles('/test/project')

expect(store.claudeProjectPath).toBe('/home/user/.claude/projects/Users-test-project')
expect(store.sessions).toHaveLength(2)
expect(store.sessions[0]!.name).toBe('session.json')
expect(store.sessions[1]!.name).toBe('data.jsonl')
expect(store.memoryFiles).toHaveLength(1)
expect(store.memoryFiles).toHaveLength(2)
expect(store.memoryFiles[0]!.name).toBe('MEMORY.md')
})

Expand All @@ -147,12 +128,12 @@ describe('useAiFilesStore', () => {
expect(store.error).toBe('Backend error')
})

it('clears sessions and memory when no claude project dir', async () => {
it('clears memory when no claude project dir', async () => {
const store = useAiFilesStore()

// Pre-populate
store.sessions = [
{ name: 'old.json', path: '/old/old.json', category: 'session', modifiedAt: 1 },
store.memoryFiles = [
{ name: 'old.md', path: '/old/old.md', category: 'memory', modifiedAt: 1 },
]

mockInvoke.mockImplementation((cmd: string) => {
Expand All @@ -163,7 +144,6 @@ describe('useAiFilesStore', () => {

await store.discoverFiles('/project')

expect(store.sessions).toEqual([])
expect(store.memoryFiles).toEqual([])
})
})
Expand Down
110 changes: 0 additions & 110 deletions src/__tests__/stores/transcript.test.ts

This file was deleted.

Loading
Loading