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
13 changes: 6 additions & 7 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
<Sidebar />
<main class="editor-area">
<div v-if="tabsStore.activeTab" class="editor-content">
<Editor v-if="editorModeStore.mode === 'wysiwyg'" ref="editorRef" />
<TranscriptViewer v-if="tabsStore.activeTab.fileType === 'transcript'" />
<Editor v-else-if="editorModeStore.mode === 'wysiwyg'" ref="editorRef" />
<SourceEditor v-else />
</div>
<div v-else class="no-tab-placeholder">
Expand All @@ -25,10 +26,7 @@
</div>
</main>
<!-- Outline panel (right side, like Typora) -->
<aside
v-if="outlineStore.visible"
class="outline-aside"
>
<aside v-if="outlineStore.visible" class="outline-aside">
<OutlinePanel @navigate="handleOutlineNavigate" />
</aside>
</div>
Expand All @@ -46,6 +44,7 @@ import { listen, 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 @@ -426,7 +425,7 @@ watch(
() => {
autoSaveStore.cancelPending()
autoSaveStore.syncStatus()
}
},
)

// Watch for active tab modification changes — trigger auto-save when content is modified
Expand All @@ -436,7 +435,7 @@ watch(
if (isModified) {
autoSaveStore.scheduleAutoSave()
}
}
},
)

onUnmounted(() => {
Expand Down
110 changes: 110 additions & 0 deletions src/__tests__/stores/transcript.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { invoke } from '@tauri-apps/api/core'
import { useTranscriptStore } from '../../stores/transcript'

const mockedInvoke = vi.mocked(invoke)

// Minimal valid JSONL transcript
const userLine = JSON.stringify({
type: 'user',
message: { role: 'user', content: 'Hello' },
timestamp: '2025-01-15T10:00:00.000Z',
uuid: 'uuid-1',
})
const assistantLine = JSON.stringify({
type: 'assistant',
message: {
role: 'assistant',
content: [{ type: 'text', text: 'Hi there!' }],
},
timestamp: '2025-01-15T10:00:05.000Z',
uuid: 'uuid-2',
})
const sampleJsonl = [userLine, assistantLine].join('\n')

describe('useTranscriptStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})

it('starts with null transcript and no loading/error', () => {
const store = useTranscriptStore()
expect(store.transcript).toBeNull()
expect(store.loading).toBe(false)
expect(store.error).toBeNull()
expect(store.currentFilePath).toBeNull()
})

it('loadTranscript reads file and parses transcript', async () => {
mockedInvoke.mockResolvedValueOnce(sampleJsonl)
const store = useTranscriptStore()

await store.loadTranscript('/path/to/session.jsonl')

expect(mockedInvoke).toHaveBeenCalledWith('read_file', { path: '/path/to/session.jsonl' })
expect(store.transcript).not.toBeNull()
expect(store.transcript!.messages).toHaveLength(2)
expect(store.currentFilePath).toBe('/path/to/session.jsonl')
expect(store.loading).toBe(false)
expect(store.error).toBeNull()
})

it('skips re-fetch if currentFilePath matches', async () => {
mockedInvoke.mockResolvedValueOnce(sampleJsonl)
const store = useTranscriptStore()

await store.loadTranscript('/path/to/session.jsonl')
await store.loadTranscript('/path/to/session.jsonl')

expect(mockedInvoke).toHaveBeenCalledTimes(1)
})

it('sets error on invoke failure', async () => {
mockedInvoke.mockRejectedValueOnce(new Error('File not found'))
const store = useTranscriptStore()

await store.loadTranscript('/bad/path.jsonl')

expect(store.error).toBe('File not found')
expect(store.transcript).toBeNull()
expect(store.loading).toBe(false)
})

it('sets error for non-Error rejection', async () => {
mockedInvoke.mockRejectedValueOnce('string error')
const store = useTranscriptStore()

await store.loadTranscript('/bad/path.jsonl')

expect(store.error).toBe('Failed to load transcript')
})

it('clear resets all state', async () => {
mockedInvoke.mockResolvedValueOnce(sampleJsonl)
const store = useTranscriptStore()

await store.loadTranscript('/path/to/session.jsonl')
store.clear()

expect(store.transcript).toBeNull()
expect(store.currentFilePath).toBeNull()
expect(store.error).toBeNull()
expect(store.loading).toBe(false)
})

it('loads a different file after clear', async () => {
mockedInvoke.mockResolvedValueOnce(sampleJsonl)
const store = useTranscriptStore()

await store.loadTranscript('/path/a.jsonl')
store.clear()

mockedInvoke.mockResolvedValueOnce(sampleJsonl)
await store.loadTranscript('/path/b.jsonl')

expect(store.currentFilePath).toBe('/path/b.jsonl')
expect(mockedInvoke).toHaveBeenCalledTimes(2)
})
})
Loading
Loading