This document is the implementation reference for how Philo stores widgets, updates them, saves them to the library, and tracks revisions.
If you are changing widget behavior, start here before editing the code. The goal is to avoid re-deriving the persistence model from WidgetView.tsx, widget-files.ts, and library.ts every time.
This covers the desktop app widget flow implemented in:
apps/desktop/src/services/widget-files.tsapps/desktop/src/services/widget-git-history.tsapps/desktop/src/services/library.tsapps/desktop/src/components/editor/extensions/widget/WidgetExtension.tsapps/desktop/src/components/editor/extensions/widget/WidgetView.tsxapps/desktop/src/components/editor/extensions/widget/WidgetHistoryPanel.tsxapps/desktop/src/components/editor/EditorBubbleMenu.tsxapps/desktop/src/components/layout/AppLayout.tsxapps/desktop/src/services/settings.ts
Philo widgets exist in five related layers:
- Note content
The note stores a widget embed like
![[widgets/<slug>-<id>.widget.md]]. - Widget file
The actual widget state lives in an individual
.widget.mdfile under the resolvedwidgets/directory. - Widget instance storage If the widget has generated persistent storage, it also gets a sidecar SQLite database next to the widget file.
- Library entry If the widget is archived to the library, it also gets a stable library entry and may get a shared component record.
- Git history mirror If widget Git history is enabled, Philo also writes normalized widget snapshots into an app-owned Git repo under the app data directory.
The important rule is:
- The widget file is the source of truth for the current widget prompt, runtime payload, saved state, and revision history.
- The widget file remains the source of truth for live widget state even when Git history is enabled.
- Unsaved widgets use file-scoped state and, when needed, a sibling SQLite sidecar.
- Archived library widgets reuse the archived widget file when that canonical file is known, so repeated library inserts share that file's state.
Widget files live at:
- vault mode:
<vaultDir>/widgets/ - default mode:
<journalDir>/widgets/
Filename format:
<slug>-<widget-id>.widget.md
Library files live at:
- vault mode:
<vaultDir>/library/ - default mode:
<journalDir>/library/
Legacy non-shared library filename format:
<slug>-<library-item-id>.component.md
Shared library entries are also backed by shared component manifests managed through library.ts.
The in-memory shape in widget-files.ts is:
interface WidgetRevisionRecord {
id: string;
createdAt: string;
prompt: string;
spec: string;
}
interface WidgetFileRecord {
id: string;
title: string;
prompt: string;
runtime: "json" | "code";
favorite: boolean;
saved: boolean;
spec: string;
source: string;
currentRevisionId: string;
revisions: WidgetRevisionRecord[];
libraryItemId?: string | null;
componentId?: string | null;
storageSchema?: SharedStorageSchema | null;
file: string;
path: string;
}Field meaning:
idStable widget file identity.titleDisplay title derived from the prompt.promptThe current persisted widget prompt.runtimeThe active widget runtime. New widgets usecode.favoriteFavorite state mirrored into the library drawer.savedWhether this widget is currently linked to a library entry.specLegacy JSON payload for old widgets.sourceThe current TSX widget source used by the code-widget runtime.currentRevisionIdPointer to the active revision inrevisions.revisionsAppend-only revision snapshots for checkpoint/rollback support.libraryItemIdStable link to the library item that owns this widget, when archived.componentIdStable link to the shared component manifest for reusable widget templates.storageSchemaGenerated storage contract for this widget instance. When non-empty, it powers the widget's sidecar SQLite database.fileNote embed target, usuallywidgets/<filename>.widget.md.pathAbsolute filesystem path.
Each widget file is plain markdown with frontmatter-style metadata, the current runtime payload, optional storage metadata, and an internal history block.
Example:
---
id: "widget-uuid"
title: "Raffle"
prompt: "Build a raffle widget"
runtime: "code"
saved: true
libraryItemId: "library-item-uuid"
componentId: "shared-component-uuid"
---
```tsx widget
export default function Widget() {
const [entries, setEntries,] = Philo.useWidgetState("entries", [],);
return (
<div style={{ padding: 16, }}>
<button
onClick={() => setEntries((current,) => [...current, { id: crypto.randomUUID(), label: "New entry", },])}
>
Add entry
</button>
<ul>
{entries.map((entry,) => <li key={entry.id}>{entry.label}</li>)}
</ul>
</div>
);
}
```
```json widget-storage
{
"tables": [
{
"name": "items",
"columns": [
{ "name": "id", "type": "integer", "primaryKey": true },
{ "name": "title", "type": "text", "notNull": true }
]
}
],
"namedQueries": [],
"namedMutations": []
}
```
```json widget-history
{
"currentRevisionId": "revision-uuid-2",
"revisions": [
{
"id": "revision-uuid-1",
"createdAt": "2026-03-17T08:00:00.000Z",
"prompt": "Build a raffle widget",
"spec": "export default function Widget() { return <div />; }"
},
{
"id": "revision-uuid-2",
"createdAt": "2026-03-17T08:05:00.000Z",
"prompt": "Build a raffle widget with a winner area",
"spec": "export default function Widget() { return <div>Winner area</div>; }"
}
]
}
```Notes:
- New widgets store their runtime payload in a
tsx widgetblock. - Legacy widgets may still carry a plain
jsonblock until rebuilt. - The optional
json widget-storageblock is the instance storage schema. If it contains tables, Philo creates a sibling.widget.sqlite3file for that widget instance. - The
json widget-historyblock is the internal checkpoint log. - Older widget files without a history block are migrated lazily when rewritten.
When widget Git history is enabled, Philo also serializes a normalized snapshot of the widget into an app-owned Git mirror. That snapshot includes:
idtitlepromptruntimesavedspecfor legacy JSON widgetssourcefor code widgetslibraryItemIdcomponentIdstorageSchema
The Git snapshot intentionally excludes:
currentRevisionIdrevisionsfavoritefilepath
This keeps Git diffs focused on material widget changes instead of checkpoint churn or local-only metadata.
When a note is saved, the widget node prefers writing an embed:
![[widgets/raffle-<id>.widget.md]]When a note is loaded, resolveWidgetEmbeds() reads the widget file and replaces the embed with a data-widget HTML placeholder for the editor runtime.
That runtime placeholder carries:
data-iddata-storage-iddata-filedata-pathdata-promptdata-runtimedata-sourcefor code widgetsdata-speconly for legacy JSON widgetsdata-storage-schemadata-saveddata-library-item-iddata-component-id
data-id is the per-node editor identity. data-storage-id is the stable widget file identity used for widget storage and file rewrites. Multiple embeds can therefore share one widget file without sharing editor event state.
Entry points:
- selection bubble menu
Build Mod-Shift-B
For a new AI-generated widget:
- Insert a temporary widget node in the editor with
loading: true. - Generate TSX widget source and storage schema together.
- Create a new
.widget.mdfile throughcreateWidgetFile(). - If the storage schema is non-empty, create the widget's sidecar SQLite file.
- Persist the first revision automatically.
- Replace the temporary node attrs with the real file-backed widget record.
- Record a Git snapshot with reason
createwhen widget Git history is enabled.
Entry point:
- toolbar refresh button
Flow:
WidgetViewbuilds a generation prompt from the saved widget prompt plus the current widget source.- The existing widget stays visible in the note.
- The widget body gets an in-place build overlay.
- The new source and storage schema are generated together.
- Existing widgets with a storage schema must keep that schema unchanged.
persistWidgetRecord()rewrites the widget file.- A new revision is appended if the prompt/source changed.
- A Git snapshot with reason
rebuildis recorded when the normalized widget snapshot changed.
Entry point:
- toolbar pencil button
Flow:
WidgetViewrequests a widget edit session.AppLayoutopens the AI composer with[Edit widget] <title>.- The user submits an edit instruction.
WidgetViewturns that instruction into a generation prompt that includes:- the persisted widget prompt
- the current widget source
- the requested change
- The widget rebuilds in place.
- The resulting prompt/source pair is persisted and appended as a new revision.
- A Git snapshot with reason
editis recorded when the normalized widget snapshot changed.
The important distinction is:
- the generation prompt can include the current source and the edit instruction
- the persisted prompt stays the canonical widget prompt stored on disk
Entry point:
- toolbar archive button on an inline widget
Flow:
WidgetView.handleSave()archives the current code widget with storage metadata.addToLibrary()creates the shared component manifest entry for the library drawer.- The widget file is rewritten with:
saved: truelibraryItemIdcomponentId- the widget instance's
storageSchema
- The editor node is updated to match the rewritten widget file.
- A Git snapshot with reason
archiveis recorded when the normalized widget snapshot changed.
Entry point:
- Library drawer
Flow:
loadLibrary()returns library items backed by saved widget files, plus shared component metadata and legacy fallbacks when needed.- If the chosen library item already has canonical widget file metadata (
file,path,storageId),AppLayoutinserts a new widget node that points at that existing widget file. - The inserted node gets a fresh editor
id, but keeps the archived widget'sstorageId,file,path,libraryItemId, andcomponentId. - If the library item does not have canonical widget file metadata,
AppLayoutfalls back to creating a new widget file from the library item. - That fallback path records a Git snapshot with reason
insert.
The normal archived-widget path now reuses the archived widget file, so repeated inserts share the same file-backed state and the same widget-sidecar SQLite database.
Entry point:
- Library drawer delete action
Flow:
removeFromLibrary()removes the library entry.markWidgetLibraryReferenceRemoved()scans widget files.- Matching widget files have their library link cleared:
saved: falselibraryItemId: nullcomponentId: nullwhen it matched the removed shared component
This keeps note widgets and library state from drifting.
Philo now tracks widget history in two parallel ways:
- Internal widget revisions in the
.widget.mdfile - Optional Git-backed widget snapshots in an app-owned mirror repo
Internal revision rules:
- Every new widget starts with one internal revision.
- Every rebuild or chat edit that changes prompt/source appends one new internal revision.
- Saving to library also appends an internal revision if it changes the persisted widget record.
- If a rewrite does not materially change the prompt/source pair, no duplicate internal revision is appended.
Git snapshot rules:
- Widget Git history is controlled by
settings.widgetGitHistoryEnabledand defaults totrue. - Git commits are only recorded for material widget flows:
create,rebuild,edit,insert,archive, andrestore. - Favorite toggles, internal-history-only rewrites, and library-reference cleanup do not create Git commits.
- Existing widgets are lazily baselined into Git history with reason
importthe first time the history panel is opened or the first time a tracked material save runs. - The Git mirror stores normalized widget snapshots, not raw live widget files and not widget SQLite sidecars.
The widget toolbar now includes a Git history action when widget Git history is enabled.
History panel behavior:
- Loads Git history lazily for the current widget file.
- Lists revisions newest-first with reason, title, and timestamp.
- Shows a unified diff for the selected revision.
- Uses the same diff renderer as the AI diff preview UI.
Restore behavior:
- Restore rewrites the live widget file in place from the selected Git snapshot.
- Restore appends a new internal widget revision for the restored prompt/source pair.
- Restore then records a new Git snapshot with reason
restore.
Restore guardrails:
- Restore is blocked if the selected snapshot's
storageSchemadiffers from the current widget's schema. - If the selected snapshot points at a missing library item or missing shared component, Philo clears
saved,libraryItemId, andcomponentIdbefore rewriting the live widget file. - Deleted-widget recovery is still out of scope. The history panel only works for existing widget files.
When changing widget code, preserve these invariants:
- The widget file stays the canonical source of truth.
savedalone is not enough to identify library linkage. UselibraryItemId.componentIdidentifies the reusable template, whilestorageIdidentifies the concrete widget file used for file-backed state.- Editor node ids and widget storage ids are intentionally different.
- Revisions are append-only snapshots of prompt/source state.
- Git snapshots are append-only snapshots of normalized widget state.
- Removing a library entry must clear saved/library references from widget files.
- Note markdown should keep storing widget embeds, not inline giant payloads.
If you touch widget persistence, verify:
- widget create still writes a
.widget.mdfile - note save/load still round-trips widget embeds
- rebuild/edit still append revisions
- Git history only records material widget changes
- history panel can load a baseline for pre-Git widgets
- restore is blocked when storage schemas differ
- restore clears dead library/shared-component references
- save-to-library still writes
saved,libraryItemId, andcomponentId - library insert reuses the canonical widget file when present
- library delete still clears matching widget files
- old widget files without history still load and rewrite correctly