2026-02-22 17:46:31 +01:00
|
|
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
2026-02-14 18:22:42 +01:00
|
|
|
import { Sidebar } from './components/Sidebar'
|
|
|
|
|
import { NoteList } from './components/NoteList'
|
|
|
|
|
import { Editor } from './components/Editor'
|
|
|
|
|
import { ResizeHandle } from './components/ResizeHandle'
|
2026-02-21 09:41:46 +01:00
|
|
|
import { CreateTypeDialog } from './components/CreateTypeDialog'
|
2026-02-14 21:17:38 +01:00
|
|
|
import { QuickOpenPalette } from './components/QuickOpenPalette'
|
2026-02-14 21:19:38 +01:00
|
|
|
import { Toast } from './components/Toast'
|
2026-02-15 13:00:10 +01:00
|
|
|
import { CommitDialog } from './components/CommitDialog'
|
2026-02-17 11:03:23 +01:00
|
|
|
import { StatusBar } from './components/StatusBar'
|
2026-02-22 13:38:18 +01:00
|
|
|
import { SettingsPanel } from './components/SettingsPanel'
|
2026-02-22 17:46:31 +01:00
|
|
|
import { GitHubVaultModal } from './components/GitHubVaultModal'
|
2026-02-17 12:10:21 +01:00
|
|
|
import { useVaultLoader } from './hooks/useVaultLoader'
|
2026-02-22 13:38:18 +01:00
|
|
|
import { useSettings } from './hooks/useSettings'
|
2026-02-21 19:15:39 +01:00
|
|
|
import { useNoteActions, generateUntitledName } from './hooks/useNoteActions'
|
2026-02-23 19:53:36 +01:00
|
|
|
import { useEditorSave } from './hooks/useEditorSave'
|
2026-02-23 20:34:12 +01:00
|
|
|
import { useCommitFlow } from './hooks/useCommitFlow'
|
2026-02-20 19:59:05 +01:00
|
|
|
import { useAppKeyboard } from './hooks/useAppKeyboard'
|
2026-02-23 12:01:46 +01:00
|
|
|
import { useViewMode } from './hooks/useViewMode'
|
2026-02-22 10:48:13 +01:00
|
|
|
import { useEntryActions } from './hooks/useEntryActions'
|
2026-02-24 00:09:19 +01:00
|
|
|
import { isTauri } from './mock-tauri'
|
2026-02-23 14:42:39 +01:00
|
|
|
import { pickFolder } from './utils/vault-dialog'
|
|
|
|
|
|
2026-02-21 10:50:50 +01:00
|
|
|
import { useKeyboardNavigation } from './hooks/useKeyboardNavigation'
|
2026-02-22 12:10:24 +01:00
|
|
|
import { useUpdater } from './hooks/useUpdater'
|
feat: auto-build, GitHub Release, and in-app updater (#14)
* ci: auto-release workflow on merge to main
Rewrite .github/workflows/release.yml to trigger on every push to main
instead of manual tag pushes. The workflow now:
- Computes version as 0.YYYYMMDD.GITHUB_RUN_NUMBER
- Builds aarch64-apple-darwin and x86_64-apple-darwin in parallel
- Merges them into a universal binary using lipo
- Creates a universal .dmg and signed updater tarball
- Generates latest.json with per-arch and universal platform entries
- Publishes a GitHub Release with auto-generated release notes
- Updates a GitHub Pages release history site (gh-pages branch)
Product decisions:
- Universal binary approach: copy arm64 .app as base, lipo the main
executable, keep everything else from arm64 (shared frameworks are
architecture-independent). This is the standard Tauri pattern.
- Per-arch updater tarballs are also uploaded so the Tauri updater can
download the correct arch-specific build (smaller download).
- Release notes are auto-generated from git log since last tag.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: github pages with release history
Use peaceiris/actions-gh-pages@v4 to deploy a release history site.
The page fetches releases.json (also deployed) and renders each release
with date, notes, and download links for .dmg files.
This handles the gh-pages branch creation automatically on first run.
The page is available at https://refactoringhq.github.io/laputa-app/
Product decision: used fetch() to load releases.json at runtime instead
of inlining it, which is cleaner and avoids shell escaping issues with
release note content. The releases.json is deployed alongside index.html.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: in-app update notification UI
Replace the old window.confirm updater with a proper React-based
update notification system:
- useUpdater hook now exposes state machine (idle → available →
downloading → ready) and actions (startDownload, openReleaseNotes,
dismiss)
- UpdateBanner component renders at the top of the app shell:
- "Available" state: shows version, Release Notes link, Update Now
button, dismiss X
- "Downloading" state: animated spinner, progress bar with percentage
- "Ready" state: Restart Now button to apply the update
- Silently checks on startup after 3s delay; fails silently on
network errors or 404
- Release Notes link opens the GitHub Pages release history site
Product decisions:
- Banner at top of app (not a modal) — non-intrusive, visible but
not blocking. User can dismiss and continue working.
- Progress bar shows during download so user knows it's working.
- Separate "Restart Now" state after download so user controls when
the app restarts (they may have unsaved work).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: updater component tests
Rewrite useUpdater hook tests and add UpdateBanner component tests:
Hook tests (10 cases):
- Starts in idle state
- Does nothing when not in Tauri
- Checks for updates after 3s delay
- Stays idle when no update available
- Transitions to available when update found
- Handles missing release body gracefully
- Stays idle on network error (fails silently)
- Dismiss returns to idle
- openReleaseNotes opens correct URL
- startDownload transitions through downloading to ready
Component tests (10 cases):
- Renders nothing when idle
- Renders nothing on error
- Shows version and buttons when available
- Update Now calls startDownload
- Release Notes calls openReleaseNotes
- Dismiss button works
- Shows progress bar during download
- Shows 0% at start of download
- Shows restart button when ready
- Restart button calls restartApp
All 457 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: auto-build-release wireframes
Copy ui-design.pen as base. Frames to be added for:
1. Update notification banner (visible state) — horizontal bar at
top of app shell with version text, Release Notes link, Update
Now button, and dismiss X
2. Update download progress state — spinner icon, progress bar with
percentage, downloading text
3. "Restart to apply" state — green accent, version text, Restart
Now button
Note: Pencil editor was not available during this session. The base
design file is committed; frames will be added when the editor is
accessible. The implemented component (UpdateBanner.tsx) serves as
the source of truth for the design.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update ARCHITECTURE.md with release/update system
Add comprehensive documentation for:
- Release pipeline (4-phase workflow: version → build → release → pages)
- Versioning scheme (0.YYYYMMDD.RUN_NUMBER)
- Universal binary strategy (lipo merge)
- Updater endpoint and latest.json manifest
- In-app update UI state machine
- GitHub Pages release history site
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rustfmt formatting
* fix: rustfmt build.rs
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:50:36 +01:00
|
|
|
import { UpdateBanner } from './components/UpdateBanner'
|
2026-02-22 13:38:18 +01:00
|
|
|
import { setApiKey } from './utils/ai-chat'
|
2026-02-17 12:10:21 +01:00
|
|
|
import type { SidebarSelection, GitCommit } from './types'
|
2026-02-22 17:46:31 +01:00
|
|
|
import type { VaultOption } from './components/StatusBar'
|
2026-02-14 18:20:07 +01:00
|
|
|
import './App.css'
|
|
|
|
|
|
2026-02-15 12:54:11 +01:00
|
|
|
// Type declaration for mock content storage
|
|
|
|
|
declare global {
|
|
|
|
|
interface Window {
|
|
|
|
|
__mockContent?: Record<string, string>
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 19:35:10 +01:00
|
|
|
const DEFAULT_SELECTION: SidebarSelection = { kind: 'filter', filter: 'all' }
|
|
|
|
|
|
2026-02-22 17:46:31 +01:00
|
|
|
const DEFAULT_VAULTS: VaultOption[] = isTauri()
|
2026-02-21 11:07:47 +01:00
|
|
|
? [
|
|
|
|
|
{ label: 'Demo v2', path: '/Users/luca/Workspace/laputa-app/demo-vault-v2' },
|
|
|
|
|
{ label: 'Laputa', path: '/Users/luca/Laputa' },
|
|
|
|
|
]
|
|
|
|
|
: [
|
|
|
|
|
{ label: 'Demo v2', path: '/Users/luca/Workspace/laputa-app/demo-vault-v2' },
|
|
|
|
|
]
|
2026-02-17 17:45:31 +01:00
|
|
|
|
2026-02-22 10:48:13 +01:00
|
|
|
function useLayoutPanels() {
|
2026-02-14 18:22:42 +01:00
|
|
|
const [sidebarWidth, setSidebarWidth] = useState(250)
|
|
|
|
|
const [noteListWidth, setNoteListWidth] = useState(300)
|
|
|
|
|
const [inspectorWidth, setInspectorWidth] = useState(280)
|
|
|
|
|
const [inspectorCollapsed, setInspectorCollapsed] = useState(false)
|
2026-02-22 10:48:13 +01:00
|
|
|
const handleSidebarResize = useCallback((delta: number) => setSidebarWidth((w) => Math.max(150, Math.min(400, w + delta))), [])
|
|
|
|
|
const handleNoteListResize = useCallback((delta: number) => setNoteListWidth((w) => Math.max(200, Math.min(500, w + delta))), [])
|
|
|
|
|
const handleInspectorResize = useCallback((delta: number) => setInspectorWidth((w) => Math.max(200, Math.min(500, w - delta))), [])
|
|
|
|
|
return { sidebarWidth, noteListWidth, inspectorWidth, inspectorCollapsed, setInspectorCollapsed, handleSidebarResize, handleNoteListResize, handleInspectorResize }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
|
const [selection, setSelection] = useState<SidebarSelection>(DEFAULT_SELECTION)
|
|
|
|
|
const layout = useLayoutPanels()
|
2026-02-14 20:53:52 +01:00
|
|
|
const [gitHistory, setGitHistory] = useState<GitCommit[]>([])
|
2026-02-21 09:41:46 +01:00
|
|
|
const [showCreateTypeDialog, setShowCreateTypeDialog] = useState(false)
|
2026-02-14 21:17:38 +01:00
|
|
|
const [showQuickOpen, setShowQuickOpen] = useState(false)
|
2026-02-14 21:19:38 +01:00
|
|
|
const [toastMessage, setToastMessage] = useState<string | null>(null)
|
2026-02-22 17:46:31 +01:00
|
|
|
const [vaultPath, setVaultPath] = useState(DEFAULT_VAULTS[0].path)
|
2026-02-20 16:06:51 +01:00
|
|
|
const [showAIChat, setShowAIChat] = useState(false)
|
2026-02-22 13:38:18 +01:00
|
|
|
const [showSettings, setShowSettings] = useState(false)
|
2026-02-22 17:46:31 +01:00
|
|
|
const [showGitHubVault, setShowGitHubVault] = useState(false)
|
|
|
|
|
const [extraVaults, setExtraVaults] = useState<VaultOption[]>([])
|
|
|
|
|
|
|
|
|
|
const allVaults = useMemo(() => [...DEFAULT_VAULTS, ...extraVaults], [extraVaults])
|
2026-02-14 21:19:38 +01:00
|
|
|
|
2026-02-17 17:45:31 +01:00
|
|
|
const vault = useVaultLoader(vaultPath)
|
2026-02-22 13:38:18 +01:00
|
|
|
const { settings, saveSettings } = useSettings()
|
|
|
|
|
|
|
|
|
|
// Sync Anthropic key from settings to localStorage for AIChatPanel
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setApiKey(settings.anthropic_key ?? '')
|
|
|
|
|
}, [settings.anthropic_key])
|
|
|
|
|
|
fix: sync in-memory entries after property/rename changes
Two propagation gaps fixed:
1. Property changes (type, status, color, etc.) now immediately sync to
the entries state via frontmatterToEntryPatch(), so sidebar, note list,
and relations panel reflect updates without restart.
2. After renaming a note, all other open tabs reload their content from
disk, picking up updated wikilinks immediately.
Design decisions:
- Used key-mapping approach (frontmatterToEntryPatch) instead of a new
parse_md_file Tauri command — simpler, works in both Tauri and mock
modes, covers all VaultEntry fields.
- Converted useNoteActions to config object to avoid excess arguments.
- Extracted findWikilinkTarget, reloadTabsAfterRename, renameToastMessage
as standalone functions to reduce hook complexity (cc 10→9).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:48:39 +01:00
|
|
|
const notes = useNoteActions({ addEntry: vault.addEntry, updateContent: vault.updateContent, entries: vault.entries, setToastMessage, updateEntry: vault.updateEntry })
|
2026-02-15 12:56:28 +01:00
|
|
|
|
2026-02-23 20:34:12 +01:00
|
|
|
const { handleSave, handleContentChange, savePendingForPath, savePending } = useEditorSave({
|
2026-02-23 19:53:36 +01:00
|
|
|
updateVaultContent: vault.updateContent,
|
|
|
|
|
setTabs: notes.setTabs,
|
|
|
|
|
setToastMessage,
|
2026-02-23 21:13:25 +01:00
|
|
|
onAfterSave: vault.loadModifiedFiles,
|
2026-02-23 19:53:36 +01:00
|
|
|
})
|
2026-02-23 14:48:21 +01:00
|
|
|
|
2026-02-23 20:34:12 +01:00
|
|
|
const commitFlow = useCommitFlow({
|
|
|
|
|
savePending,
|
|
|
|
|
loadModifiedFiles: vault.loadModifiedFiles,
|
|
|
|
|
commitAndPush: vault.commitAndPush,
|
|
|
|
|
setToastMessage,
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-22 10:48:13 +01:00
|
|
|
const entryActions = useEntryActions({
|
|
|
|
|
entries: vault.entries,
|
|
|
|
|
updateEntry: vault.updateEntry,
|
|
|
|
|
handleUpdateFrontmatter: notes.handleUpdateFrontmatter,
|
|
|
|
|
handleDeleteProperty: notes.handleDeleteProperty,
|
|
|
|
|
setToastMessage,
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-21 19:15:39 +01:00
|
|
|
// Immediate note creation — no dialog, just create and open
|
|
|
|
|
const handleCreateNoteImmediate = useCallback((type?: string) => {
|
|
|
|
|
const noteType = type || 'Note'
|
|
|
|
|
notes.handleCreateNote(generateUntitledName(vault.entries, noteType), noteType)
|
|
|
|
|
window.dispatchEvent(new CustomEvent('laputa:focus-editor'))
|
|
|
|
|
}, [vault.entries, notes])
|
2026-02-21 09:41:46 +01:00
|
|
|
|
2026-02-17 17:45:31 +01:00
|
|
|
const handleSwitchVault = useCallback((path: string) => {
|
|
|
|
|
setVaultPath(path)
|
|
|
|
|
setSelection(DEFAULT_SELECTION)
|
|
|
|
|
setGitHistory([])
|
|
|
|
|
notes.closeAllTabs()
|
|
|
|
|
}, [notes])
|
|
|
|
|
|
2026-02-22 17:46:31 +01:00
|
|
|
const handleVaultCloned = useCallback((path: string, label: string) => {
|
|
|
|
|
setExtraVaults(prev => {
|
|
|
|
|
if (prev.some(v => v.path === path)) return prev
|
|
|
|
|
return [...prev, { label, path }]
|
|
|
|
|
})
|
|
|
|
|
handleSwitchVault(path)
|
|
|
|
|
setToastMessage(`Vault "${label}" cloned and opened`)
|
|
|
|
|
}, [handleSwitchVault])
|
|
|
|
|
|
2026-02-23 14:42:39 +01:00
|
|
|
const addAndSwitchVault = useCallback((path: string, label: string) => {
|
|
|
|
|
setExtraVaults(prev => {
|
|
|
|
|
if (prev.some(v => v.path === path)) return prev
|
|
|
|
|
return [...prev, { label, path }]
|
|
|
|
|
})
|
|
|
|
|
handleSwitchVault(path)
|
|
|
|
|
}, [handleSwitchVault])
|
|
|
|
|
|
|
|
|
|
const handleOpenLocalFolder = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const path = await pickFolder('Open vault folder')
|
|
|
|
|
if (!path) return
|
|
|
|
|
const label = path.split('/').pop() || 'Local Vault'
|
|
|
|
|
addAndSwitchVault(path, label)
|
|
|
|
|
setToastMessage(`Vault "${label}" opened`)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to open local folder:', err)
|
|
|
|
|
setToastMessage(`Failed to open folder: ${err}`)
|
|
|
|
|
}
|
|
|
|
|
}, [addAndSwitchVault])
|
|
|
|
|
|
2026-02-14 20:53:52 +01:00
|
|
|
useEffect(() => {
|
2026-02-22 10:48:13 +01:00
|
|
|
if (!notes.activeTabPath) { setGitHistory([]); return }
|
2026-02-17 12:10:21 +01:00
|
|
|
vault.loadGitHistory(notes.activeTabPath).then(setGitHistory)
|
fix: resolve React hooks/compiler ESLint errors
- Wrap ref assignments in useEffect to fix refs-during-render in
useTabManagement, useKeyboardNavigation, and Editor
- Rewrite useAppKeyboard to define keyMap inside useEffect, eliminating
ref access during render
- Add eslint-disable for react-hooks/set-state-in-effect on legitimate
dialog reset patterns (CommitDialog, CreateNoteDialog, CreateTypeDialog,
QuickOpenPalette, AIChatPanel, useVaultLoader)
- Add eslint-disable for react-hooks/static-components on icon lookups
(NoteItem, NoteList PinnedCard) — stateless icon components from a
static map
- Add eslint-disable for react-hooks/purity on Date.now() in
NoteItem TrashDateLine — intentionally memoized on trashedAt
- Remove unused `model` param from buildSystemPrompt (ai-chat.ts) and
update callers
- Fix exhaustive-deps: suppress vault object dep in App.tsx git history
effect (vault object is unstable, loadGitHistory is the real dep)
- Remove unused `modifiedFiles` from useNoteListData params and caller
- Add missing `ref` to Sidebar useOutsideClick deps
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 14:06:51 +01:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- vault object is unstable; loadGitHistory is the actual dep
|
2026-02-17 12:10:21 +01:00
|
|
|
}, [notes.activeTabPath, vault.loadGitHistory])
|
2026-02-14 20:53:52 +01:00
|
|
|
|
2026-02-21 09:41:46 +01:00
|
|
|
const openCreateTypeDialog = useCallback(() => {
|
|
|
|
|
setShowCreateTypeDialog(true)
|
2026-02-20 23:49:30 +01:00
|
|
|
}, [])
|
|
|
|
|
|
2026-02-21 09:41:46 +01:00
|
|
|
const handleCreateType = useCallback((name: string) => {
|
|
|
|
|
notes.handleCreateType(name)
|
|
|
|
|
setToastMessage(`Type "${name}" created`)
|
2026-02-22 10:48:13 +01:00
|
|
|
}, [notes])
|
2026-02-21 17:29:42 +01:00
|
|
|
|
2026-02-23 19:53:36 +01:00
|
|
|
const handleRenameTab = useCallback(async (path: string, newTitle: string) => {
|
|
|
|
|
// Save any pending content before renaming so the file on disk is up to date
|
|
|
|
|
await savePendingForPath(path)
|
2026-02-21 19:22:44 +01:00
|
|
|
notes.handleRenameNote(path, newTitle, vaultPath, vault.replaceEntry)
|
2026-02-23 19:53:36 +01:00
|
|
|
}, [notes, vaultPath, vault, savePendingForPath])
|
2026-02-21 19:22:44 +01:00
|
|
|
|
2026-02-23 12:01:46 +01:00
|
|
|
const { setViewMode } = useViewMode()
|
|
|
|
|
|
2026-02-20 19:59:05 +01:00
|
|
|
useAppKeyboard({
|
|
|
|
|
onQuickOpen: () => setShowQuickOpen(true),
|
2026-02-21 19:15:39 +01:00
|
|
|
onCreateNote: handleCreateNoteImmediate,
|
2026-02-23 19:53:36 +01:00
|
|
|
onSave: handleSave,
|
2026-02-22 13:38:18 +01:00
|
|
|
onOpenSettings: () => setShowSettings(true),
|
2026-02-22 10:48:13 +01:00
|
|
|
onTrashNote: entryActions.handleTrashNote,
|
|
|
|
|
onArchiveNote: entryActions.handleArchiveNote,
|
2026-02-23 12:01:46 +01:00
|
|
|
onSetViewMode: setViewMode,
|
2026-02-20 19:59:05 +01:00
|
|
|
activeTabPathRef: notes.activeTabPathRef,
|
|
|
|
|
handleCloseTabRef: notes.handleCloseTabRef,
|
|
|
|
|
})
|
2026-02-14 21:14:16 +01:00
|
|
|
|
feat: auto-build, GitHub Release, and in-app updater (#14)
* ci: auto-release workflow on merge to main
Rewrite .github/workflows/release.yml to trigger on every push to main
instead of manual tag pushes. The workflow now:
- Computes version as 0.YYYYMMDD.GITHUB_RUN_NUMBER
- Builds aarch64-apple-darwin and x86_64-apple-darwin in parallel
- Merges them into a universal binary using lipo
- Creates a universal .dmg and signed updater tarball
- Generates latest.json with per-arch and universal platform entries
- Publishes a GitHub Release with auto-generated release notes
- Updates a GitHub Pages release history site (gh-pages branch)
Product decisions:
- Universal binary approach: copy arm64 .app as base, lipo the main
executable, keep everything else from arm64 (shared frameworks are
architecture-independent). This is the standard Tauri pattern.
- Per-arch updater tarballs are also uploaded so the Tauri updater can
download the correct arch-specific build (smaller download).
- Release notes are auto-generated from git log since last tag.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: github pages with release history
Use peaceiris/actions-gh-pages@v4 to deploy a release history site.
The page fetches releases.json (also deployed) and renders each release
with date, notes, and download links for .dmg files.
This handles the gh-pages branch creation automatically on first run.
The page is available at https://refactoringhq.github.io/laputa-app/
Product decision: used fetch() to load releases.json at runtime instead
of inlining it, which is cleaner and avoids shell escaping issues with
release note content. The releases.json is deployed alongside index.html.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: in-app update notification UI
Replace the old window.confirm updater with a proper React-based
update notification system:
- useUpdater hook now exposes state machine (idle → available →
downloading → ready) and actions (startDownload, openReleaseNotes,
dismiss)
- UpdateBanner component renders at the top of the app shell:
- "Available" state: shows version, Release Notes link, Update Now
button, dismiss X
- "Downloading" state: animated spinner, progress bar with percentage
- "Ready" state: Restart Now button to apply the update
- Silently checks on startup after 3s delay; fails silently on
network errors or 404
- Release Notes link opens the GitHub Pages release history site
Product decisions:
- Banner at top of app (not a modal) — non-intrusive, visible but
not blocking. User can dismiss and continue working.
- Progress bar shows during download so user knows it's working.
- Separate "Restart Now" state after download so user controls when
the app restarts (they may have unsaved work).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: updater component tests
Rewrite useUpdater hook tests and add UpdateBanner component tests:
Hook tests (10 cases):
- Starts in idle state
- Does nothing when not in Tauri
- Checks for updates after 3s delay
- Stays idle when no update available
- Transitions to available when update found
- Handles missing release body gracefully
- Stays idle on network error (fails silently)
- Dismiss returns to idle
- openReleaseNotes opens correct URL
- startDownload transitions through downloading to ready
Component tests (10 cases):
- Renders nothing when idle
- Renders nothing on error
- Shows version and buttons when available
- Update Now calls startDownload
- Release Notes calls openReleaseNotes
- Dismiss button works
- Shows progress bar during download
- Shows 0% at start of download
- Shows restart button when ready
- Restart button calls restartApp
All 457 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: auto-build-release wireframes
Copy ui-design.pen as base. Frames to be added for:
1. Update notification banner (visible state) — horizontal bar at
top of app shell with version text, Release Notes link, Update
Now button, and dismiss X
2. Update download progress state — spinner icon, progress bar with
percentage, downloading text
3. "Restart to apply" state — green accent, version text, Restart
Now button
Note: Pencil editor was not available during this session. The base
design file is committed; frames will be added when the editor is
accessible. The implemented component (UpdateBanner.tsx) serves as
the source of truth for the design.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update ARCHITECTURE.md with release/update system
Add comprehensive documentation for:
- Release pipeline (4-phase workflow: version → build → release → pages)
- Versioning scheme (0.YYYYMMDD.RUN_NUMBER)
- Universal binary strategy (lipo merge)
- Updater endpoint and latest.json manifest
- In-app update UI state machine
- GitHub Pages release history site
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rustfmt formatting
* fix: rustfmt build.rs
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:50:36 +01:00
|
|
|
const { status: updateStatus, actions: updateActions } = useUpdater()
|
2026-02-22 12:10:24 +01:00
|
|
|
|
2026-02-21 10:50:50 +01:00
|
|
|
useKeyboardNavigation({
|
|
|
|
|
tabs: notes.tabs,
|
|
|
|
|
activeTabPath: notes.activeTabPath,
|
|
|
|
|
entries: vault.entries,
|
|
|
|
|
selection,
|
|
|
|
|
allContent: vault.allContent,
|
|
|
|
|
onSwitchTab: notes.handleSwitchTab,
|
|
|
|
|
onReplaceActiveTab: notes.handleReplaceActiveTab,
|
|
|
|
|
onSelectNote: notes.handleSelectNote,
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-17 12:10:21 +01:00
|
|
|
const activeTab = notes.tabs.find((t) => t.entry.path === notes.activeTabPath) ?? null
|
2026-02-15 13:00:10 +01:00
|
|
|
|
2026-02-14 18:20:07 +01:00
|
|
|
return (
|
2026-02-17 11:03:23 +01:00
|
|
|
<div className="app-shell">
|
feat: auto-build, GitHub Release, and in-app updater (#14)
* ci: auto-release workflow on merge to main
Rewrite .github/workflows/release.yml to trigger on every push to main
instead of manual tag pushes. The workflow now:
- Computes version as 0.YYYYMMDD.GITHUB_RUN_NUMBER
- Builds aarch64-apple-darwin and x86_64-apple-darwin in parallel
- Merges them into a universal binary using lipo
- Creates a universal .dmg and signed updater tarball
- Generates latest.json with per-arch and universal platform entries
- Publishes a GitHub Release with auto-generated release notes
- Updates a GitHub Pages release history site (gh-pages branch)
Product decisions:
- Universal binary approach: copy arm64 .app as base, lipo the main
executable, keep everything else from arm64 (shared frameworks are
architecture-independent). This is the standard Tauri pattern.
- Per-arch updater tarballs are also uploaded so the Tauri updater can
download the correct arch-specific build (smaller download).
- Release notes are auto-generated from git log since last tag.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: github pages with release history
Use peaceiris/actions-gh-pages@v4 to deploy a release history site.
The page fetches releases.json (also deployed) and renders each release
with date, notes, and download links for .dmg files.
This handles the gh-pages branch creation automatically on first run.
The page is available at https://refactoringhq.github.io/laputa-app/
Product decision: used fetch() to load releases.json at runtime instead
of inlining it, which is cleaner and avoids shell escaping issues with
release note content. The releases.json is deployed alongside index.html.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: in-app update notification UI
Replace the old window.confirm updater with a proper React-based
update notification system:
- useUpdater hook now exposes state machine (idle → available →
downloading → ready) and actions (startDownload, openReleaseNotes,
dismiss)
- UpdateBanner component renders at the top of the app shell:
- "Available" state: shows version, Release Notes link, Update Now
button, dismiss X
- "Downloading" state: animated spinner, progress bar with percentage
- "Ready" state: Restart Now button to apply the update
- Silently checks on startup after 3s delay; fails silently on
network errors or 404
- Release Notes link opens the GitHub Pages release history site
Product decisions:
- Banner at top of app (not a modal) — non-intrusive, visible but
not blocking. User can dismiss and continue working.
- Progress bar shows during download so user knows it's working.
- Separate "Restart Now" state after download so user controls when
the app restarts (they may have unsaved work).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: updater component tests
Rewrite useUpdater hook tests and add UpdateBanner component tests:
Hook tests (10 cases):
- Starts in idle state
- Does nothing when not in Tauri
- Checks for updates after 3s delay
- Stays idle when no update available
- Transitions to available when update found
- Handles missing release body gracefully
- Stays idle on network error (fails silently)
- Dismiss returns to idle
- openReleaseNotes opens correct URL
- startDownload transitions through downloading to ready
Component tests (10 cases):
- Renders nothing when idle
- Renders nothing on error
- Shows version and buttons when available
- Update Now calls startDownload
- Release Notes calls openReleaseNotes
- Dismiss button works
- Shows progress bar during download
- Shows 0% at start of download
- Shows restart button when ready
- Restart button calls restartApp
All 457 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* design: auto-build-release wireframes
Copy ui-design.pen as base. Frames to be added for:
1. Update notification banner (visible state) — horizontal bar at
top of app shell with version text, Release Notes link, Update
Now button, and dismiss X
2. Update download progress state — spinner icon, progress bar with
percentage, downloading text
3. "Restart to apply" state — green accent, version text, Restart
Now button
Note: Pencil editor was not available during this session. The base
design file is committed; frames will be added when the editor is
accessible. The implemented component (UpdateBanner.tsx) serves as
the source of truth for the design.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update ARCHITECTURE.md with release/update system
Add comprehensive documentation for:
- Release pipeline (4-phase workflow: version → build → release → pages)
- Versioning scheme (0.YYYYMMDD.RUN_NUMBER)
- Universal binary strategy (lipo merge)
- Updater endpoint and latest.json manifest
- In-app update UI state machine
- GitHub Pages release history site
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rustfmt formatting
* fix: rustfmt build.rs
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:50:36 +01:00
|
|
|
<UpdateBanner status={updateStatus} actions={updateActions} />
|
2026-02-17 11:03:23 +01:00
|
|
|
<div className="app">
|
2026-02-22 10:48:13 +01:00
|
|
|
<div className="app__sidebar" style={{ width: layout.sidebarWidth }}>
|
2026-02-23 20:34:12 +01:00
|
|
|
<Sidebar entries={vault.entries} selection={selection} onSelect={setSelection} onSelectNote={notes.handleSelectNote} onCreateType={handleCreateNoteImmediate} onCreateNewType={openCreateTypeDialog} onCustomizeType={entryActions.handleCustomizeType} onReorderSections={entryActions.handleReorderSections} modifiedCount={vault.modifiedFiles.length} onCommitPush={commitFlow.openCommitDialog} />
|
2026-02-17 11:03:23 +01:00
|
|
|
</div>
|
2026-02-22 10:48:13 +01:00
|
|
|
<ResizeHandle onResize={layout.handleSidebarResize} />
|
|
|
|
|
<div className="app__note-list" style={{ width: layout.noteListWidth }}>
|
2026-02-23 20:12:54 +01:00
|
|
|
<NoteList entries={vault.entries} selection={selection} selectedNote={activeTab?.entry ?? null} allContent={vault.allContent} modifiedFiles={vault.modifiedFiles} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={notes.handleReplaceActiveTab} onCreateNote={handleCreateNoteImmediate} />
|
2026-02-17 11:03:23 +01:00
|
|
|
</div>
|
2026-02-22 10:48:13 +01:00
|
|
|
<ResizeHandle onResize={layout.handleNoteListResize} />
|
2026-02-17 11:03:23 +01:00
|
|
|
<div className="app__editor">
|
|
|
|
|
<Editor
|
2026-02-17 12:10:21 +01:00
|
|
|
tabs={notes.tabs}
|
|
|
|
|
activeTabPath={notes.activeTabPath}
|
|
|
|
|
entries={vault.entries}
|
|
|
|
|
onSwitchTab={notes.handleSwitchTab}
|
|
|
|
|
onCloseTab={notes.handleCloseTab}
|
2026-02-21 17:24:01 +01:00
|
|
|
onReorderTabs={notes.handleReorderTabs}
|
2026-02-17 12:10:21 +01:00
|
|
|
onNavigateWikilink={notes.handleNavigateWikilink}
|
|
|
|
|
onLoadDiff={vault.loadDiff}
|
2026-02-21 22:27:18 +01:00
|
|
|
onLoadDiffAtCommit={vault.loadDiffAtCommit}
|
2026-02-17 12:10:21 +01:00
|
|
|
isModified={vault.isFileModified}
|
2026-02-21 19:15:39 +01:00
|
|
|
onCreateNote={handleCreateNoteImmediate}
|
2026-02-22 10:48:13 +01:00
|
|
|
inspectorCollapsed={layout.inspectorCollapsed}
|
|
|
|
|
onToggleInspector={() => layout.setInspectorCollapsed((c) => !c)}
|
|
|
|
|
inspectorWidth={layout.inspectorWidth}
|
|
|
|
|
onInspectorResize={layout.handleInspectorResize}
|
2026-02-17 11:03:23 +01:00
|
|
|
inspectorEntry={activeTab?.entry ?? null}
|
|
|
|
|
inspectorContent={activeTab?.content ?? null}
|
2026-02-17 12:10:21 +01:00
|
|
|
allContent={vault.allContent}
|
2026-02-17 11:03:23 +01:00
|
|
|
gitHistory={gitHistory}
|
2026-02-17 12:10:21 +01:00
|
|
|
onUpdateFrontmatter={notes.handleUpdateFrontmatter}
|
|
|
|
|
onDeleteProperty={notes.handleDeleteProperty}
|
|
|
|
|
onAddProperty={notes.handleAddProperty}
|
2026-02-20 16:06:51 +01:00
|
|
|
showAIChat={showAIChat}
|
|
|
|
|
onToggleAIChat={() => setShowAIChat(c => !c)}
|
2026-02-21 10:07:02 +01:00
|
|
|
vaultPath={vaultPath}
|
2026-02-22 10:48:13 +01:00
|
|
|
onTrashNote={entryActions.handleTrashNote}
|
|
|
|
|
onRestoreNote={entryActions.handleRestoreNote}
|
|
|
|
|
onArchiveNote={entryActions.handleArchiveNote}
|
|
|
|
|
onUnarchiveNote={entryActions.handleUnarchiveNote}
|
2026-02-21 19:22:44 +01:00
|
|
|
onRenameTab={handleRenameTab}
|
2026-02-23 19:53:36 +01:00
|
|
|
onContentChange={handleContentChange}
|
2026-02-17 11:03:23 +01:00
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-14 18:20:07 +01:00
|
|
|
</div>
|
2026-02-24 00:09:19 +01:00
|
|
|
<StatusBar noteCount={vault.entries.length} modifiedCount={vault.modifiedFiles.length} vaultPath={vaultPath} vaults={allVaults} onSwitchVault={handleSwitchVault} onOpenSettings={() => setShowSettings(true)} onOpenLocalFolder={handleOpenLocalFolder} onConnectGitHub={() => setShowGitHubVault(true)} hasGitHub={!!settings.github_token} />
|
2026-02-14 21:19:38 +01:00
|
|
|
<Toast message={toastMessage} onDismiss={() => setToastMessage(null)} />
|
2026-02-22 10:48:13 +01:00
|
|
|
<QuickOpenPalette open={showQuickOpen} entries={vault.entries} onSelect={notes.handleSelectNote} onClose={() => setShowQuickOpen(false)} />
|
|
|
|
|
<CreateTypeDialog open={showCreateTypeDialog} onClose={() => setShowCreateTypeDialog(false)} onCreate={handleCreateType} />
|
2026-02-23 20:34:12 +01:00
|
|
|
<CommitDialog open={commitFlow.showCommitDialog} modifiedCount={vault.modifiedFiles.length} onCommit={commitFlow.handleCommitPush} onClose={commitFlow.closeCommitDialog} />
|
2026-02-22 13:38:18 +01:00
|
|
|
<SettingsPanel open={showSettings} settings={settings} onSave={saveSettings} onClose={() => setShowSettings(false)} />
|
2026-02-22 17:46:31 +01:00
|
|
|
<GitHubVaultModal
|
|
|
|
|
open={showGitHubVault}
|
|
|
|
|
githubToken={settings.github_token}
|
|
|
|
|
onClose={() => setShowGitHubVault(false)}
|
|
|
|
|
onVaultCloned={handleVaultCloned}
|
|
|
|
|
onOpenSettings={() => { setShowGitHubVault(false); setShowSettings(true) }}
|
|
|
|
|
/>
|
2026-02-14 18:22:42 +01:00
|
|
|
</div>
|
2026-02-14 18:20:07 +01:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default App
|