Skip to content
Open
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
56 changes: 56 additions & 0 deletions app/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,66 @@ const createWindow = (token: string): void => {
}
}

// GitHub repo that hosts releases (public; no token needed for the releases API).
const REPO = 'FishPain/fish-notes'

// Compare dotted numeric versions: true if a > b (e.g. "0.1.2" > "0.1.1").
const isNewer = (a: string, b: string): boolean => {
const pa = a.split('.').map(Number)
const pb = b.split('.').map(Number)
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) > (pb[i] || 0)
}
return false
}

// Query the latest GitHub release and compare to the running version.
const checkForUpdate = async (): Promise<{
current: string
latest: string | null
hasUpdate: boolean
dmgUrl?: string
releaseUrl?: string
}> => {
const current = app.getVersion()
try {
const res = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
headers: { 'User-Agent': 'FishNotes', Accept: 'application/vnd.github+json' }
})
if (!res.ok) return { current, latest: null, hasUpdate: false }
const rel = (await res.json()) as {
tag_name: string
html_url: string
assets: { name: string; browser_download_url: string }[]
}
const latest = rel.tag_name.replace(/^v/, '')
const dmg = rel.assets.find((a) => a.name.endsWith('.dmg'))
return { current, latest, hasUpdate: isNewer(latest, current), dmgUrl: dmg?.browser_download_url, releaseUrl: rel.html_url }
} catch {
return { current, latest: null, hasUpdate: false }
}
}

// Download a release DMG to the Downloads folder and open (mount) it for the user to install.
const downloadUpdate = async (url: string): Promise<{ ok: boolean; path?: string }> => {
try {
const res = await fetch(url, { headers: { 'User-Agent': 'FishNotes' } })
if (!res.ok) return { ok: false }
const dest = join(app.getPath('downloads'), url.split('/').pop() || 'Fish Notes.dmg')
await writeFile(dest, Buffer.from(await res.arrayBuffer()))
shell.openPath(dest)
return { ok: true, path: dest }
} catch {
return { ok: false }
}
}

app.whenReady().then(() => {
const token = loadOrCreateToken(app.getPath('userData'))
ipcMain.handle('capture-region', captureRegion)
ipcMain.handle('save-image', (_e, { dataUrl, name }: { dataUrl: string; name: string }) => saveImage(dataUrl, name))
ipcMain.handle('updates:check', () => checkForUpdate())
ipcMain.handle('updates:download', (_e, url: string) => downloadUpdate(url))

// Settings: read effective config (defaults + env + saved), write overrides to
// userData/settings.json, test the proxy, and relaunch to apply.
Expand Down
6 changes: 6 additions & 0 deletions app/electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ contextBridge.exposeInMainWorld('engine', {
// App version (from package.json via app.getVersion() in main), shown in the sidebar.
contextBridge.exposeInMainWorld('appInfo', { version: arg('app-version') })

// Check GitHub releases for a newer version and download+open its DMG.
contextBridge.exposeInMainWorld('updates', {
check: () => ipcRenderer.invoke('updates:check'),
download: (url: string) => ipcRenderer.invoke('updates:download', url)
})

// Native screen-region capture (main runs macOS `screencapture`).
contextBridge.exposeInMainWorld('capture', {
region: () => ipcRenderer.invoke('capture-region'),
Expand Down
2 changes: 1 addition & 1 deletion app/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8" />
<title>Canvas Notes</title>
<title>Fish Notes</title>
</head>
<body>
<div id="root"></div>
Expand Down
4 changes: 4 additions & 0 deletions app/renderer/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ declare global {
interface Window {
engine: { baseUrl: string; token: string }
appInfo: { version: string }
updates: {
check: () => Promise<{ current: string; latest: string | null; hasUpdate: boolean; dmgUrl?: string; releaseUrl?: string }>
download: (url: string) => Promise<{ ok: boolean; path?: string }>
}
capture: {
region: () => Promise<{ cancelled?: boolean; pngBase64?: string; error?: string; appName?: string; url?: string; windowTitle?: string }>
saveImage: (dataUrl: string, name: string) => Promise<{ saved?: boolean; path?: string; cancelled?: boolean }>
Expand Down
47 changes: 47 additions & 0 deletions app/renderer/settings-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ export const SettingsDialog = ({ open, onClose }: { open: boolean; onClose: () =
const [test, setTest] = useState<{ ok: boolean; count?: number; status?: number } | null>(null)
const [testing, setTesting] = useState(false)
const [saved, setSaved] = useState(false)
const [update, setUpdate] = useState<Awaited<ReturnType<typeof window.updates.check>> | null>(null)
const [checking, setChecking] = useState(false)
const [dl, setDl] = useState<'idle' | 'downloading' | 'opened' | 'error'>('idle')

// Load config when opened, then quietly fetch the model list to fill the dropdowns.
useEffect(() => {
if (!open) return
setTest(null)
setSaved(false)
setUpdate(null)
setDl('idle')
window.settings.get().then(async (c) => {
setCfg(c)
const r = await window.settings.test(c.baseUrl, c.apiKey)
Expand All @@ -40,6 +45,20 @@ export const SettingsDialog = ({ open, onClose }: { open: boolean; onClose: () =
setSaved(true)
}

const checkUpdates = async (): Promise<void> => {
setChecking(true)
setDl('idle')
setUpdate(await window.updates.check())
setChecking(false)
}

const downloadUpdate = async (): Promise<void> => {
if (!update?.dmgUrl) return
setDl('downloading')
const r = await window.updates.download(update.dmgUrl)
setDl(r.ok ? 'opened' : 'error')
}

return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>Settings</DialogTitle>
Expand Down Expand Up @@ -89,6 +108,34 @@ export const SettingsDialog = ({ open, onClose }: { open: boolean; onClose: () =
Open data folder
</Button>

<Divider />
<Stack spacing={1}>
<Button variant="outlined" onClick={checkUpdates} disabled={checking} sx={{ alignSelf: 'flex-start', textTransform: 'none' }}>
{checking ? 'Checking…' : 'Check for updates'}
</Button>
{update && !update.hasUpdate && (
<Typography variant="caption" sx={{ opacity: 0.7 }}>
{update.latest ? `You're on the latest version (v${update.current}).` : "Couldn't check right now — try again later."}
</Typography>
)}
{update?.hasUpdate && update.dmgUrl && (
<Alert
severity="info"
action={
<Button color="inherit" size="small" onClick={downloadUpdate} disabled={dl === 'downloading'}>
{dl === 'downloading' ? 'Downloading…' : 'Download'}
</Button>
}
>
Update available: v{update.latest} (you have v{update.current}).
</Alert>
)}
{dl === 'opened' && (
<Alert severity="success">Installer opened — drag Fish Notes to Applications, then reopen. First launch: right-click → Open.</Alert>
)}
{dl === 'error' && <Alert severity="error">Download failed — try again later.</Alert>}
</Stack>

{test && (
<Alert severity={test.ok ? 'success' : 'error'}>
{test.ok ? `Connected — ${test.count} models available` : `Failed${test.status ? ` (HTTP ${test.status})` : ' to reach the proxy'}`}
Expand Down
2 changes: 1 addition & 1 deletion docs/design/plans/2026-09-18-phase1b-capturer.md
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ The pure modules above are tested; this task is the browser wiring, verified by
```json
{
"manifest_version": 3,
"name": "Canvas Notes Capturer",
"name": "Fish Notes Capturer",
"version": "0.1.0",
"permissions": ["activeTab", "scripting", "storage", "contextMenus"],
"host_permissions": ["http://localhost:7645/*"],
Expand Down
6 changes: 3 additions & 3 deletions docs/design/plans/2026-09-18-phase2a-electron-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ No unit tests (UI wiring, verified by running the app). Keep components small; M
<html>
<head>
<meta charset="utf-8" />
<title>Canvas Notes</title>
<title>Fish Notes</title>
</head>
<body>
<div id="root"></div>
Expand Down Expand Up @@ -416,7 +416,7 @@ export const App = (): React.ReactElement => {

return (
<Box sx={{ maxWidth: 900, mx: 'auto', p: 3 }}>
<Typography variant="h5" sx={{ mb: 2 }}>Canvas Notes</Typography>
<Typography variant="h5" sx={{ mb: 2 }}>Fish Notes</Typography>
<Stack direction="row" spacing={1} sx={{ mb: 2 }}>
<TextField
fullWidth
Expand Down Expand Up @@ -487,7 +487,7 @@ git commit -m "feat: react shell — search, ask, captures, token display"
- [ ] **Step 1: Rebuild for Electron, then launch the app** (Node 24)

Run: `cd app && npm run rebuild:electron && npm run dev`
Expected: `electron-rebuild` recompiles `better-sqlite3` for Electron's ABI, then an Electron window opens showing "Canvas Notes"; the console logs `engine on http://127.0.0.1:7645`. (To run the test suite again afterward, first `npm run rebuild:node`.)
Expected: `electron-rebuild` recompiles `better-sqlite3` for Electron's ABI, then an Electron window opens showing "Fish Notes"; the console logs `engine on http://127.0.0.1:7645`. (To run the test suite again afterward, first `npm run rebuild:node`.)

- [ ] **Step 2: Verify corpus flows in the window**
- The footer shows the extension token + endpoint.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Canvas Notes — Design
# Fish Notes — Design

**Date:** 2026-09-18
**Status:** Draft for review (redesigned: search-first, canvas-based, RAG-grounded)
Expand Down
Loading