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: 3 additions & 2 deletions src/main/__tests__/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ vi.mock('fs/promises', () => ({
}))

import { readFile, writeFile, rename, mkdir } from 'fs/promises'
import { loadState, saveState, type PersistedState } from '../store'
import { loadState, saveState, DEFAULT_AUTO_SUMMARY, type PersistedState } from '../store'

const mockedReadFile = vi.mocked(readFile)
const mockedWriteFile = vi.mocked(writeFile)
Expand All @@ -34,7 +34,8 @@ describe('loadState', () => {
nextProjectId: 1,
sessions: [],
activeSessionIndex: null,
gridCols: 2
gridCols: 2,
autoSummary: DEFAULT_AUTO_SUMMARY
})
})

Expand Down
27 changes: 24 additions & 3 deletions src/main/store.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { readFile, writeFile, rename, mkdir } from 'fs/promises'
import { join } from 'path'
import { homedir } from 'os'
import type { PrInfo, IssueInfo } from '../shared/types'
import type { PrInfo, IssueInfo, AutoSummarySettings } from '../shared/types'
import { DEFAULT_AUTO_SUMMARY } from '../shared/types'

const STORE_DIR = join(homedir(), '.konductor')
const STATE_FILE = join(STORE_DIR, 'state.json')
Expand All @@ -23,13 +24,17 @@ export interface SessionData {
issue?: IssueInfo
}

export type { AutoSummarySettings }
export { DEFAULT_AUTO_SUMMARY }

export interface PersistedState {
projects: ProjectData[]
activeProjectId: string | null
nextProjectId: number
sessions: SessionData[]
activeSessionIndex: number | null
gridCols?: 1 | 2
autoSummary?: AutoSummarySettings
}

const DEFAULT_STATE: PersistedState = {
Expand All @@ -38,20 +43,36 @@ const DEFAULT_STATE: PersistedState = {
nextProjectId: 1,
sessions: [],
activeSessionIndex: null,
gridCols: 2
gridCols: 2,
autoSummary: DEFAULT_AUTO_SUMMARY
}

export async function loadState(): Promise<PersistedState> {
try {
const raw = await readFile(STATE_FILE, 'utf-8')
const parsed = JSON.parse(raw) as Partial<PersistedState>
const rawAuto = parsed.autoSummary
const autoSummary: AutoSummarySettings = {
enabled:
typeof rawAuto?.enabled === 'boolean' ? rawAuto.enabled : DEFAULT_AUTO_SUMMARY.enabled,
debounceSeconds:
typeof rawAuto?.debounceSeconds === 'number' && rawAuto.debounceSeconds >= 0
? rawAuto.debounceSeconds
: DEFAULT_AUTO_SUMMARY.debounceSeconds,
minTurns:
typeof rawAuto?.minTurns === 'number' && rawAuto.minTurns >= 1
? rawAuto.minTurns
: DEFAULT_AUTO_SUMMARY.minTurns
}

return {
projects: Array.isArray(parsed.projects) ? parsed.projects : [],
activeProjectId: parsed.activeProjectId ?? null,
nextProjectId: typeof parsed.nextProjectId === 'number' ? parsed.nextProjectId : 1,
sessions: Array.isArray(parsed.sessions) ? parsed.sessions : [],
activeSessionIndex: parsed.activeSessionIndex ?? null,
gridCols: parsed.gridCols === 1 ? 1 : 2
gridCols: parsed.gridCols === 1 ? 1 : 2,
autoSummary
}
} catch {
return { ...DEFAULT_STATE }
Expand Down
12 changes: 10 additions & 2 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ function App(): React.JSX.Element {
resizeSession,
updateSessionSummary,
gridCols,
setGridCols
setGridCols,
autoSummary,
setAutoSummary
} = useSessions()

const {
Expand Down Expand Up @@ -376,7 +378,13 @@ function App(): React.JSX.Element {
/>
)}

{effectiveViewMode === 'settings' && <SettingsView onBack={() => setViewMode('grid')} />}
{effectiveViewMode === 'settings' && (
<SettingsView
onBack={() => setViewMode('grid')}
autoSummary={autoSummary}
onAutoSummaryChange={setAutoSummary}
/>
)}
</main>

{worktreeProject && (
Expand Down
82 changes: 81 additions & 1 deletion src/renderer/src/components/SettingsView.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import { ChevronLeftIcon, RefreshIcon } from './Icons'
import type { UpdateStatus, LogEntry } from '../../../preload/index'
import type { AutoSummarySettings } from '../../../shared/types'

const api = window.konductorAPI

interface SettingsViewProps {
onBack: () => void
autoSummary: AutoSummarySettings
onAutoSummaryChange: (settings: AutoSummarySettings) => void
}

const levelColors: Record<LogEntry['level'], string> = {
Expand All @@ -30,7 +33,11 @@ function formatTime(ts: number): string {
})
}

export default function SettingsView({ onBack }: SettingsViewProps): React.JSX.Element {
export default function SettingsView({
onBack,
autoSummary,
onAutoSummaryChange
}: SettingsViewProps): React.JSX.Element {
const [updateStatus, setUpdateStatus] = useState<UpdateStatus | null>(null)
const [checking, setChecking] = useState(false)
const [logs, setLogs] = useState<LogEntry[]>([])
Expand Down Expand Up @@ -152,6 +159,79 @@ export default function SettingsView({ onBack }: SettingsViewProps): React.JSX.E
</div>
</section>

{/* Auto Summary Section */}
<section>
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">
Auto Summary
</h3>
<div className="bg-surface-raised border border-surface-border rounded-lg p-4 space-y-4">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-gray-200">Enable auto-summary</div>
<div className="text-[10px] text-gray-600 mt-0.5">
Automatically regenerate session summaries using AI as the conversation develops
</div>
</div>
<button
onClick={() =>
onAutoSummaryChange({ ...autoSummary, enabled: !autoSummary.enabled })
}
className={`relative w-9 h-5 rounded-full transition-colors ${autoSummary.enabled ? 'bg-accent' : 'bg-gray-600'}`}
>
<span
className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${autoSummary.enabled ? 'translate-x-4' : ''}`}
/>
</button>
</div>

{autoSummary.enabled && (
<>
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-gray-200">Debounce interval</div>
<div className="text-[10px] text-gray-600 mt-0.5">
Minimum seconds between summary regenerations per session
</div>
</div>
<input
type="number"
min={0}
max={600}
value={autoSummary.debounceSeconds}
onChange={(e) => {
const v = parseInt(e.target.value, 10)
if (!isNaN(v) && v >= 0)
onAutoSummaryChange({ ...autoSummary, debounceSeconds: v })
}}
className="w-20 bg-surface border border-surface-border rounded px-2 py-1 text-sm text-gray-200 text-right focus:outline-none focus:border-gray-500 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
</div>

<div className="flex items-center justify-between">
<div>
<div className="text-sm text-gray-200">Minimum turns</div>
<div className="text-[10px] text-gray-600 mt-0.5">
Claude turns required before generating a summary (resets after each
generation)
</div>
</div>
<input
type="number"
min={1}
max={50}
value={autoSummary.minTurns}
onChange={(e) => {
const v = parseInt(e.target.value, 10)
if (!isNaN(v) && v >= 1) onAutoSummaryChange({ ...autoSummary, minTurns: v })
}}
className="w-20 bg-surface border border-surface-border rounded px-2 py-1 text-sm text-gray-200 text-right focus:outline-none focus:border-gray-500 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
</div>
</>
)}
</div>
</section>

{/* App Logs Section */}
<section className="flex flex-col min-h-0" style={{ height: 'calc(100vh - 280px)' }}>
<div className="flex items-center justify-between mb-3">
Expand Down
Loading
Loading