Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
eac4208
feat(views): open a note's view as its own leaf
Leo310 Sep 17, 2026
6a6dbe6
fix(views): leaf follows renames fully, resolves its fence by title, …
Leo310 Sep 17, 2026
88d3a13
fix(views): a leaf's frame spans the full pane width
Leo310 Sep 17, 2026
8999da7
fix(views): a leaf whose note is missing is titled by the note
Leo310 Sep 17, 2026
c040ccc
feat(views): standalone `.view` files — leaf, embeds, agent-writable,…
Leo310 Sep 17, 2026
f543bbf
fix(views): source editor refuses to overwrite a newer file; embeds d…
Leo310 Sep 17, 2026
07ef60f
feat(views): note links inside a view preview like links in a note
Leo310 Sep 17, 2026
5f642d9
fix(views): a modified click on a note link inside a view opens it li…
Leo310 Sep 17, 2026
0473651
fix(views): frame runtime broken by a formatter-rewritten escape; Cmd…
Leo310 Sep 17, 2026
68dc489
fix(views): a note link href with a percent sign is a valid path
Leo310 Sep 17, 2026
3d3ff93
refactor(widgets): rename "view" to "widget" everywhere
Leo310 Sep 17, 2026
26db94f
test: expected note-tool message after the widget rename
Leo310 Sep 17, 2026
d813ee8
fix(widgets): leaf class names match the stylesheet again
Leo310 Sep 17, 2026
81ce15b
feat(widgets): expand a widget from the chat to a near-fullscreen modal
Leo310 Sep 17, 2026
975f090
feat(widgets): the agent can read a .widget file, and the skill says …
Leo310 Sep 17, 2026
57ccf0e
feat(widgets): a .widget file is findable by its title and description
Leo310 Sep 17, 2026
394e793
fix(widgets): own icon instead of Canvas's
Leo310 Sep 17, 2026
b3a4bf9
fix(widgets): use the component icon
Leo310 Sep 17, 2026
3c1b08c
feat(widgets): a widget can choose its own icon
Leo310 Sep 17, 2026
9a8f62d
fix(widgets): the tab icon field must not shadow View.icon
Leo310 Sep 17, 2026
2e7929d
docs(widgets): icon guidance names the subject, not a list
Leo310 Sep 17, 2026
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
4 changes: 2 additions & 2 deletions src/agent/integrations/pluginIntegrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,15 @@ const BUNDLED_CORE_SKILL_ICONS: Record<string, string> = {
"manage-notes": "file-pen",
web: "globe",
"manage-skills": "wand-2",
views: "layout-dashboard",
widgets: "component", // = DEFAULT_WIDGET_ICON in widget/widgetIcon.ts
};

/**
* Fixed display order for the S2B built-in core skills (the 4 former capabilities). Used by
* both the agent editor's Core Skills list and the agents-summary icon strip so the two never
* drift. Core-plugin skills (Canvas, Bases, …) are ranked after all of these.
*/
const S2B_CORE_SKILL_ORDER = ["explore-vault", "manage-notes", "web", "manage-skills", "views"];
const S2B_CORE_SKILL_ORDER = ["explore-vault", "manage-notes", "web", "manage-skills", "widgets"];

/**
* Sort rank for a core-category skill: listed S2B built-ins first in `S2B_CORE_SKILL_ORDER`,
Expand Down
2 changes: 1 addition & 1 deletion src/agent/tools/loadSkill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export interface LoadSkillToolOptions {
/**
* Live status of a community plugin a skill lists under `metadata.optionalPlugins`.
* Appended to the loaded body so a skill whose *best* path depends on another
* plugin (the views skill's Dataview queries) can tell the model up front whether
* plugin (the widgets skill's Dataview queries) can tell the model up front whether
* that path exists, instead of letting it find out from a failed result.
*/
pluginStatus?: (pluginId: string) => { status: CommunityPluginStatus; displayName: string };
Expand Down
28 changes: 19 additions & 9 deletions src/agent/tools/manageNotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,20 @@ const editSchema = z.object({
.describe("Replace all occurrences instead of requiring a single unique match. Default false."),
});

/** What the agent may create, edit, move and delete: notes, and standalone widgets (`.widget`, see views/widget). */
const AGENT_WRITABLE_EXTENSIONS = [".md", ".widget"];
const isAgentWritablePath = (path: string): boolean => AGENT_WRITABLE_EXTENSIONS.some((ext) => path.endsWith(ext));

const createOperationSchema = z.object({
type: z.literal("create"),
path: z
.string()
.describe("Vault-relative path for the new markdown note. Must end in .md. Example: Notes/my-note.md"),
content: z.string().describe("Full markdown content for the new note"),
.describe(
"Vault-relative path for the new file. Must end in .md (a note) or .widget (a standalone widget: frontmatter, then HTML). Example: Notes/my-note.md",
),
content: z
.string()
.describe("Full content for the new file (markdown for a note; a widget fence's body for a .widget)"),
});

const REPLACE_PENDING_DESCRIPTION =
Expand Down Expand Up @@ -65,7 +73,9 @@ const deleteOperationSchema = z.object({
const moveOperationSchema = z.object({
type: z.literal("move"),
path: z.string().describe("Current file path or wiki link reference to move"),
newPath: z.string().describe("Destination vault-relative path for the markdown note. Must end in .md."),
newPath: z
.string()
.describe("Destination vault-relative path. Must end in .md (a note) or .widget (a standalone widget)."),
});

const replaceOperationSchema = z.object({
Expand Down Expand Up @@ -147,9 +157,9 @@ function validateExistingMarkdownFile(
}

const file = result.file;
if (!file.path.endsWith(".md")) {
if (!isAgentWritablePath(file.path)) {
return {
error: `Error in operation ${operationNumber}: Only markdown files (.md) can be ${action}d. "${file.path}" is not a markdown file.`,
error: `Error in operation ${operationNumber}: Only notes (.md) and widgets (.widget) can be ${action}d. "${file.path}" is neither.`,
};
}

Expand Down Expand Up @@ -510,8 +520,8 @@ async function stageNoteOperations(
const duplicateError = ensureUniqueTarget(seenPaths, normalizedPath, operationNumber);
if (duplicateError) return duplicateError;

if (!normalizedPath.endsWith(".md")) {
return `Error in operation ${operationNumber}: Only markdown files (.md) can be created. Got: "${normalizedPath}"`;
if (!isAgentWritablePath(normalizedPath)) {
return `Error in operation ${operationNumber}: Only notes (.md) and widgets (.widget) can be created. Got: "${normalizedPath}"`;
}

if (!store.isPathAllowed(normalizedPath)) {
Expand Down Expand Up @@ -617,8 +627,8 @@ async function stageNoteOperations(
if (sourceDuplicateError) return sourceDuplicateError;

const normalizedNewPath = normalizePath(operation.newPath);
if (!normalizedNewPath.endsWith(".md")) {
return `Error in operation ${operationNumber}: Only markdown files (.md) can be moved. Got destination "${normalizedNewPath}"`;
if (!isAgentWritablePath(normalizedNewPath)) {
return `Error in operation ${operationNumber}: Only notes (.md) and widgets (.widget) can be moved. Got destination "${normalizedNewPath}"`;
}

const destinationDuplicateError = ensureUniqueTarget(seenPaths, normalizedNewPath, operationNumber);
Expand Down
18 changes: 9 additions & 9 deletions src/components/ui/MarkdownRenderer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { Component, Keymap, MarkdownRenderer, loadMathJax } from "obsidian";
import { onDestroy } from "svelte";
import { getPlugin } from "../../stores/state.svelte";
import { VIEW_READY_EVENT } from "../../genview/viewFrame";
import { WIDGET_READY_EVENT } from "../../widget/widgetFrame";
import { findSealableEnd } from "../../utils/streamingMarkdown";
import { openTagSearch } from "../../utils/tagSearch";
import { VIEW_TYPE_CHAT } from "../../views/chat/Chat";
Expand Down Expand Up @@ -159,14 +159,14 @@ function normalizeLinks(containerEl: HTMLElement) {
// thread for the duration of a long reply (#482). The tail's staging element
// carries `s2b-md-tail` while it renders, so a block processor can tell "still
// being written" (show a placeholder) from "sealed" (render for real) — see
// `genview/registerViewBlocks.ts`.
// `widget/registerWidgetBlocks.ts`.
//
// Outside streaming — a settled reply's first render, and the moment a streamed
// reply settles — the content is rendered as one document, so every construct
// resolves exactly as a whole-document parse does (reference definitions, HTML
// blocks and loose lists that a seam between segments would cut). That render is
// double-buffered: it happens in a hidden wrapper while the previous DOM stays on
// screen, and swaps in once the wrapper's view frames report ready (capped), so
// screen, and swaps in once the wrapper's widget frames report ready (capped), so
// settling never blanks a frame that was already showing. The wrapper is then
// unwrapped so the container stays flat (call sites style with direct-child
// selectors such as `[&>p]`) — unless it holds an iframe, which a DOM move would
Expand All @@ -175,7 +175,7 @@ function normalizeLinks(containerEl: HTMLElement) {
const TAIL_CLASS = "s2b-md-tail";
const DOC_CLASS = "s2b-md-doc";
const DOC_PENDING_CLASS = "s2b-md-doc-pending";
/** Upper bound on waiting for a settled document's view frames before swapping it in. */
/** Upper bound on waiting for a settled document's widget frames before swapping it in. */
const VIEW_READY_GRACE_MS = 2000;

let latest = { content: "", sourcePath: "", enableMath: true, streaming: false };
Expand All @@ -191,7 +191,7 @@ let tailNodes: ChildNode[] = [];
let settled = false;
/**
* Owners of the render children Obsidian's processors attach to rendered blocks
* (Dataview tables, `s2b-view` frames, embeds). Handing the renderer the plugin
* (Dataview tables, `s2b-widget` frames, embeds). Handing the renderer the plugin
* itself kept every such child alive until plugin unload; these are unloaded when
* the DOM they belong to goes away — the tail's on every tail render, the
* document's on reset, swap and destroy.
Expand Down Expand Up @@ -296,20 +296,20 @@ async function renderDocument(text: string, path: string) {
settled = true;
}

/** Resolve once every view frame under `root` has reported ready, or after the grace period. */
/** Resolve once every widget frame under `root` has reported ready, or after the grace period. */
function waitForViews(root: HTMLElement): Promise<void> {
let pending = root.querySelectorAll(".s2b-view-frame:not([data-s2b-view-ready])").length;
let pending = root.querySelectorAll(".s2b-widget-frame:not([data-s2b-widget-ready])").length;
if (pending === 0) return Promise.resolve();
return new Promise((resolve) => {
const finish = () => {
root.removeEventListener(VIEW_READY_EVENT, onReady);
root.removeEventListener(WIDGET_READY_EVENT, onReady);
window.clearTimeout(timer);
resolve();
};
const onReady = () => {
if (--pending <= 0) finish();
};
root.addEventListener(VIEW_READY_EVENT, onReady);
root.addEventListener(WIDGET_READY_EVENT, onReady);
const timer = window.setTimeout(finish, VIEW_READY_GRACE_MS);
});
}
Expand Down
167 changes: 0 additions & 167 deletions src/genview/ViewRenderChild.ts

This file was deleted.

Loading
Loading