Skip to content

feat(widgets): standalone .widget files — own leaf, embeds, agent-writable, link previews - #506

Merged
Leo310 merged 21 commits into
mainfrom
feat/gen-view-leaf
Sep 17, 2026
Merged

Leo310 merged 21 commits into
mainfrom
feat/gen-view-leaf

Conversation

@Leo310

@Leo310 Leo310 commented Sep 17, 2026 •

Copy link
Copy Markdown
Member

What

The "custom view" half of the widgets feature (renamed from "view" in this PR: it collided with reading view, workspace views and Bases' views, and said nothing about the thing itself). A widget kept on its own is a .widget file: exactly what a s2b-widget fence body holds (optional frontmatter, then HTML), bound to a FileView that fills the pane. It re-renders when the file changes on disk (so an agent edit accepted through the review flow lands there), follows renames and deletes like any file view, and — since Obsidian has no editor for the extension — carries a minimal source mode of its own (textarea, save/cancel, Cmd/Ctrl-S; refuses to save over a file that changed meanwhile) behind a header action. ![[Name.widget]] embeds and hover previews render through the embed registry the .chat previews already use.

The chat toolbar's "save as note" and "open as view" collapse into one button: save as a .widget file, which opens it. Copy-as-block stays for inline use.

Note links inside widgets preview like links in a note. Any element with data-note="Path.md" (or an <a href> holding a vault path) is a note link: click opens it (modifier-aware), hover reports its box, and the host lays an invisible proxy over that box and hands it to page preview under an "S2B Widgets" hover source. Pressing Cmd/Ctrl while already hovering re-triggers, matching Obsidian's behaviour for links in notes.

Why a file: a fence in a note put its HTML/JS into Obsidian's search and into the plugin's retrieval index. A .widget file is indexed by neither, and fences that do live in notes are replaced by a (widget: <title>) marker before indexing, so a note holding a dashboard stays findable by name without its code becoming chunks. manage_notes accepts .widget paths so the agent creates and revises widgets through the same staged review as notes; the widgets skill (renamed from views, unreleased) documents the file form and the link attribute.

Also fixed on the way: the frame runtime had been broken by a formatter-rewritten escape (a unit test now parses both frame scripts as JavaScript), and the render child gains a fill mode.

Caveats: Obsidian Sync only syncs unknown file types if "sync all other types" is enabled; links written inside a .widget file are not parsed by the metadata cache (links to the file work).

How I tested it

bun run check, format, lint, test pass. Live in the S2B WT2 slot vault on macOS desktop: a s2b-widget fence in a note renders; ![[X.widget]] embeds render at content height; opening a .widget file gives the leaf with the frame filling the pane and the edit/refresh actions, and the source editor round-trips. Link previews verified with trusted pointer and key events driven through the debugger protocol: hovering a link inside a frame with Cmd held opens Obsidian's popover, and pressing Cmd while already hovering opens it too. Not yet exercised live: the chat-toolbar save button end to end, an agent creating a .widget through manage_notes and its review, workspace restore after restart, and mobile.

AI assistance: Claude Code (Fable 5.1) designed and wrote the change from Leo's brief, the file-first decision and the rename he made in discussion; Leo has tested intermediate builds live and reported the issues fixed here.

Checklist

  • bun run check, bun run format, bun run lint, and bun run test pass locally
  • I tried the change in a real Obsidian vault (or explained above why that isn't applicable)
  • I read CONTRIBUTING.md, including the section on AI assistance
  • If this adds a provider, a bundled skill, a built-in tool, or changes manifest.json: I noted that the docs site needs updating (see "Documentation" in CONTRIBUTING.md) — the widgets skill and the .widget file type need documenting on the site; feat(views): render agent-generated views as sandboxed frames #505's merged views naming (fence s2b-view, views skill, viewsFolder) is superseded by this PR.

@greptile-apps

greptile-apps Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no actionable issue remains from the changes since the previous review.

Summary

This PR replaces the earlier custom-view feature with standalone widgets and adds the supporting file, rendering, editing, embedding, indexing, and agent workflows.

  • Registers .widget files as dedicated Obsidian views with source editing and external-change protection.
  • Renders s2b-widget fences, standalone files, embeds, hover previews, and modifier-aware note links through a sandboxed frame.
  • Allows manage_notes to stage reviewed .widget operations.
  • Restricts widget retrieval content to title and description while stripping widget source from indexed Markdown.
  • Renames the bundled skill and settings from views to widgets.
  • The only change since the previous review expands bundled-skill guidance for choosing subject-specific Lucide icons; no new correctness issue was identified.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Chat[Chat s2b-widget fence] --> Renderer[WidgetRenderChild]
  File[Standalone .widget file] --> View[WidgetView]
  View --> Renderer
  Embed[Note embed or hover preview] --> EmbedHandler[Widget embed handler]
  EmbedHandler --> Renderer
  Renderer --> Frame[Sandboxed widget frame]
  Frame --> Links[Note links and previews]
  Frame --> Queries[Live vault queries]
  Agent[manage_notes] --> Review[Pending-change review]
  Review --> File
  File --> Index[Title and description indexing]
Loading

Reviews (19) · Last reviewed commit: "docs(widgets): icon guidance names the s..."

Comment thread src/views/gen-view/GenView.ts Outdated
Comment thread src/views/gen-view/GenView.ts Outdated
Comment thread src/genview/registerViewBlocks.ts Outdated
@Leo310
Leo310 changed the base branch from feat/gen-view to main September 17, 2026 12:46
@Leo310
Leo310 changed the base branch from main to feat/gen-view September 17, 2026 12:47
Base automatically changed from feat/gen-view to main September 17, 2026 12:49
Leo310 and others added 2 commits September 17, 2026 14:49
A new workspace view type shows one `s2b-view` fence of a note filling the
pane, with no note chrome. The note stays the source of truth: the leaf
re-renders when the note changes on disk (which is how an agent edit accepted
through the review flow lands), follows renames, and persists its target
(note path + fence index) with the workspace layout. Header actions open the
source note and refresh.

Entry points: a hover-revealed corner button on any rendered view in a note,
a third toolbar button in chat that saves the block (once) and opens it, and
an "Open note as view" command on the active note.

The render child gains a `fill` mode — the frame takes its container's height
and ignores auto-sizing — and `viewFences.ts` locates a note's top-level
fences (nested example fences excluded) so a block can find its own index
from the section info the processor receives.

Co-Authored-By: Claude <noreply@anthropic.com>
…saves once

Rename: the renderer resolves queries and note links against its source path,
so the leaf now re-renders after a rename instead of only updating its state.

Fence identity: the persisted ordinal alone let an edit that inserted or
removed an earlier fence silently redirect the leaf. The fence's title is
stored beside the ordinal; a fence with that title wins, the ordinal is the
fallback for untitled fences, and a target that no longer exists (including
a retitled one, indistinguishable from a redirect) shows a missing-view
state rather than another view. Reopening from the note re-targets. Opening
an already-showing leaf re-applies the fresh target.

Chat toolbar: both save buttons share one in-flight save promise, so two
quick activations cannot create two notes; a failed save is forgotten so a
retry is possible.

Co-Authored-By: Claude <noreply@anthropic.com>
Leo310 and others added 3 commits September 17, 2026 14:54
The frame width leaves room for its border; in a leaf the border is 0, so the
variable it reads is 0 there too and no 2px gap remains.

Co-Authored-By: Claude <noreply@anthropic.com>
The header was built before the state arrived and the not-found path never
refreshed it, so the tab read "View" instead of the note's name.

Co-Authored-By: Claude <noreply@anthropic.com>
… out of the index

Views become files, not fences in notes, when kept on their own. A `.view`
file holds exactly a fence's body (optional frontmatter, then HTML) and is
bound to a FileView that fills the pane; it re-renders when the file changes
on disk, follows renames and deletes like any file view, and — since
Obsidian has no editor for the extension — carries a minimal source mode of
its own (textarea, save/cancel, Cmd/Ctrl-S) behind a header action.
`![[Name.view]]` embeds and hover previews render through the embed
registry the `.chat` previews already use.

The chat toolbar's "save as note" and "open as view" collapse into one:
save as a `.view` file, which opens it. The corner button on fences in
notes and the note-index targeting (ordinal + title) go away with the
note-backed leaf they served.

Why a file: a fence in a note put its HTML and JavaScript into Obsidian's
search and, worse, the plugin's retrieval index; a `.view` file is indexed
by neither. Fences in notes stay for inline use, and their bodies are now
replaced by a `(view: <title>)` marker before indexing so a note that holds
a dashboard is still findable by name without its code becoming chunks.
`manage_notes` accepts `.view` paths so the agent creates and revises views
through the same staged review as notes.

Co-Authored-By: Claude <noreply@anthropic.com>
@Leo310 Leo310 changed the title feat(views): open a note's view as its own leaf feat(views): standalone .view files — own leaf, embeds, agent-writable Sep 17, 2026
Comment thread src/views/widget/WidgetView.ts
Comment thread src/views/widget/widgetEmbed.ts
Leo310 and others added 2 commits September 17, 2026 15:29
…rop stale loads

The source editor remembers the file's mtime when it loaded the text. A
change that lands meanwhile (an accepted agent edit, sync) is announced in
the editor's bar and makes Save refuse rather than write the older copy
over it; Cancel reloads. The embed renderer tracks a generation per load
and on unload, so an older read that finishes late cannot replace newer
content or install a child into an unloaded embed.

Co-Authored-By: Claude <noreply@anthropic.com>
Any element with data-note="<path>" is a note link: the frame runtime opens
it on click and reports its box on hover; the host lays an invisible proxy
over that box (the frame's content is unreachable from the host) and hands
it to Obsidian's page preview as the link, under a registered hover source.
The proxy takes pointer events so the popover's own hover tracking works
natively, opens the note on click (modifier-aware), and removes itself when
the pointer leaves it. The skill's example and API notes use data-note.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread src/genview/viewFrame.ts Outdated
Leo310 and others added 2 commits September 17, 2026 15:47
…ke one in a note

The runtime forwards the click's modifier keys with open-note and the host
maps them through Keymap.isModEvent, so Cmd/Ctrl-click opens a new tab and
the alt/shift variants a split or window — regardless of whether the hover
proxy was in place for that click.

Co-Authored-By: Claude <noreply@anthropic.com>
… after hover; parse test

The note-link regex in the runtime template literal used an escaped slash
pair; the formatter collapsed the escapes and the literal turned them into
a comment marker, ending the regex early. The runtime then failed to parse
in every frame — no s2b global, no ready, no resize, no hover — which is
what made link previews never trigger. The regex now uses a character
class with no backslashes, and a unit test parses both the inner runtime
and the outer relay script as JavaScript so a break like this fails the
suite instead of the frame.

Page preview can be set to require Cmd/Ctrl per source, and for a link in
a note it reacts to the key being pressed while already hovering. The
proxy now does the same: a modifier keydown while it is up re-triggers the
preview with the current modifier state.

Also treats an anchor whose href is a vault path (not a URL, fragment or
scheme) as a note link, the shape a model writes unprompted, alongside
data-note. Verified with a trusted pointer and key events driven through
the debugger protocol: hovering a link inside a frame with Cmd held, or
pressing Cmd while hovering, opens Obsidian's page-preview popover.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread src/genview/viewFrame.ts Outdated
Leo310 and others added 3 commits September 17, 2026 16:28
decodeURIComponent threw on raw vault paths such as "50% off.md"; decoding
is now attempted and the raw href used when it is not valid encoding.

Co-Authored-By: Claude <noreply@anthropic.com>
"View" collided with Obsidian's own vocabulary — reading view, workspace
views, and Bases, where every file has "views" in exactly the user-facing
sense — and said nothing about what the thing is. "Widget" describes it: a
self-contained interactive UI element that renders in place, embeds in
notes, and can stand alone in a pane. Nothing had shipped, so the rename is
total: the `s2b-widget` fence, the `.widget` file and its view type, the
`widgets` core skill, the "Widgets folder" setting, the page-preview hover
source, the CSS classes, the bridge tag, the code module and the tests.
Obsidian's own "view" terms (FileView, reading view, view types) are
untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@Leo310 Leo310 changed the title feat(views): standalone .view files — own leaf, embeds, agent-writable feat(widgets): standalone .widget files — own leaf, embeds, agent-writable, link previews Sep 17, 2026
Comment thread src/views/widget/WidgetView.ts Outdated
Leo310 and others added 5 commits September 17, 2026 21:04
The rename's word pass turned s2b-widget-view into s2b-widget-widget in the
widget view's element classes while the stylesheet kept s2b-widget-view, so
the pane body and source editor lost their layout rules.

Co-Authored-By: Claude <noreply@anthropic.com>
A third toolbar button, "Expand", opens the widget in a modal sized to
almost the whole window for a closer look without saving it. It renders a
fresh copy in fill mode — an iframe cannot be moved in the DOM without
reloading — and tears it down on close; Esc and the close button work as
in any modal. Exposed as `plugin.openWidgetModal(spec, sourcePath)` so
other entry points can reuse it.

Co-Authored-By: Claude <noreply@anthropic.com>
…how to revise one

read_content only reads extensions on the shared text allow-list, so the
agent could edit a saved widget through manage_notes but not see its
current source first. `.widget` is on the list now, and the widgets skill
spells out the revise flow: read, stage a find/replace or rewrite, the user
reviews the diff, and the open pane and embeds re-render on accept. Widget
files are deliberately absent from search, so it also says to ask for the
path or list the widgets folder.

Co-Authored-By: Claude <noreply@anthropic.com>
Widget files were kept out of the indexes entirely so their markup and
script could not become retrieval chunks — which also meant the agent
could not find one by name. They are indexable now, but contribute only
their frontmatter `title` and the new optional `description` line; the
body never enters the index. The skill asks for a description on every
widget and points search_notes at it for revisions.

Co-Authored-By: Claude <noreply@anthropic.com>
layout-dashboard is the Canvas icon; widgets (the file view's tab and the
core skill) now use app-window, verified to resolve in the bundled icon set.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread src/utils/fileFiltering.ts
Leo310 and others added 4 commits September 17, 2026 21:48
Co-Authored-By: Claude <noreply@anthropic.com>
`icon:` in the frontmatter names a Lucide icon shown on the widget's tab and
in the chat toolbar; the default stays `component`, and a name Obsidian's
icon set does not know falls back to it rather than leaving a blank tab.
The skill suggests fitting icons (chart-column, calendar, list-todo, gauge…).

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The skill's icon examples read as an allow-list, and a home dashboard came
out with a gauge. It now says any Lucide name works and that the icon
should match the widget's subject, with examples framed that way.

Co-Authored-By: Claude <noreply@anthropic.com>
@Leo310
Leo310 merged commit b9c0778 into main Sep 17, 2026
3 checks passed
@Leo310
Leo310 deleted the feat/gen-view-leaf branch September 17, 2026 20:09
Leo310 added a commit that referenced this pull request Sep 18, 2026
Widgets: the agent renders charts, tables and dashboards in the chat as
sandboxed frames fed by live Dataview queries, with Plotly for 2D/3D plots
(#505); standalone .widget files with their own pane, embeds and link
previews (#506); widgets shown in the chat first, Rename and Edit source in
the tab menu (#507); agent edits to a widget reviewed as the rendered result
(#508).

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant