Make wikilinks and tags clickable in focus view#46
Conversation
Prompted by discussion #35 — users have no way to create person notes from the command palette. Design mirrors the existing Create Project pattern with template support.
Matches the createProject pattern which calls processWithTemplater after file creation, enabling Templater syntax in person templates.
Auto-select sphere when only one is configured
When adding a focus item from the SphereView, saving the focus file triggered a metadata cache change that caused a full view refresh, resetting scroll to top. Instead, focus file changes now trigger a lightweight CSS-only update that toggles sphere-action-in-focus classes on existing DOM elements without re-rendering. Fixes #41
The CSS-only approach for focus file changes broke sphere view updates when items were completed from the Focus view, because the focus file change was the only reliable trigger for a full refresh (Obsidian's metadata cache doesn't fire for checkbox body changes). Instead, preserve scroll position by loading sphere data before emptying the container, so the empty-and-rebuild happens in one synchronous block with no async gap (no visible flash). Scroll position is saved and restored around the refresh.
The fire-and-forget renderActionItem calls mean the DOM isn't fully built when scrollTop is set, causing the browser to clamp the value. Use MutationObserver to wait for 50ms of DOM stability before restoring the scroll position.
Instead of re-rendering and restoring scroll (which causes a flash), set a flag before saving focus items so the metadata cache handler skips the auto-refresh. The CSS class update already happens locally. External focus changes (e.g. completing items in FocusView) still trigger a full refresh as before.
Focus file changes now trigger CSS-only updates in the sphere view (toggling sphere-action-in-focus classes) instead of a full re-render. When an item is completed in FocusView, a flow:action-completed workspace event removes the specific action from the sphere view DOM. This eliminates the jarring flash when interacting with the focus view while the sphere view is visible alongside it.
FocusView now triggers a flow:action-waiting workspace event when converting an action to waiting-for. SphereView listens and re-renders the specific action's content with the handshake emoji, avoiding a full view refresh.
Unregister existing workspace event handlers before registering new ones in registerWorkspaceEvents, matching the pattern used by registerMetadataCacheListener. Also strengthen the cleanup test assertion from toBeDefined to toHaveBeenCalled.
Preserve scroll position when adding actions to Focus
AGENTS.md was significantly out of date — it described AI-powered inbox processing that no longer exists, referenced deleted files (gtd-processor, flow-coach-view), and was missing many current components. Also removes project-title-prompt.ts and its test, which were unused since AI inbox processing was removed.
Resolve conflicts by keeping beta's features (context tags, discussion item on person creation) while incorporating main's additions (personsFolderPath setting, People Folder UI, scroll preservation, targeted focus updates, Templater integration).
Use MarkdownRenderer.renderMarkdown() instead of setText() for action text in FocusView. This makes wikilinks like [[Note]] and tags like #context/home clickable in the focus pane. Click handlers check the event target so link clicks are handled by Obsidian rather than navigating to the action's source file. Fixes #37 (item 3)
MarkdownRenderer creates the HTML elements but doesn't set up click handlers in custom ItemView contexts. Add explicit handling: - Internal links (wikilinks): open linked note via workspace.openLinkText - Tags: open Obsidian's global search with the tag query - Other links (external URLs): let browser handle - Non-link text: navigate to the action's source file Extract shared handleRenderedTextClick method to avoid duplication across renderItem, renderPinnedItem, and renderCompletedItem.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/sphere-view.ts (1)
748-789:⚠️ Potential issue | 🟡 MinorPrevent link clicks from toggling focus in action items.
Now that action text is rendered as markdown, clicks on internal links/tags will bubble to the list item handler and toggle focus. Guard the handler to ignore anchor clicks so links stay clickable without changing focus state.
Suggested fix
item.addEventListener("click", async (e) => { + const target = e.target; + if (target instanceof HTMLElement && target.closest("a")) { + return; + } // Capture element reference before any async operations const clickedElement = e.currentTarget as HTMLElement;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/sphere-view.ts` around lines 748 - 789, The click handler attached to item should ignore clicks that originate from anchors so internal markdown links/tags remain clickable; inside the item.addEventListener("click", ...) callback, check the event target (e.g., const target = e.target as HTMLElement) and if target.closest("a") (or target instanceof HTMLAnchorElement) return early, leaving link behavior untouched, then continue using the existing finalLineResult, isOnFocus, removeFromFocus and addToFocus logic as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/plans/2026-02-16-create-person-command-design.md`:
- Around line 1-47: The person template and modal logic must produce
Flow-compliant markdown: update the default template referenced by
personTemplateFilePath and the NewPersonModal creation flow to include YAML
frontmatter with creation-date, priority, tags (include project/person or
project/* as appropriate), status, and optional parent-project, plus the
required Title, Description, and Next actions sections; ensure NewPersonModal
replaces {{name}}, {{date}}, {{time}} and also inserts a Title line ("#
{{name}}"), a Description paragraph placeholder, and a "## Next actions"
section, and add a preflight check in the create-person command flow to
validate/normalize frontmatter fields and error if the target file already
exists.
In `@docs/plans/2026-02-16-create-person-command.md`:
- Around line 11-15: The document jumps from a top-level heading to `###` which
violates MD001; update the heading hierarchy by adding a `##` section heading
above the Task headings or promote each `### Task 1: Add settings fields` (and
subsequent Task headings) to `##` so headings increment by one; locate and edit
the heading lines in docs/plans/2026-02-16-create-person-command.md (e.g., the
"Task 1: Add settings fields" heading) and change the markdown level
accordingly.
In `@src/focus-view.ts`:
- Around line 910-943: The click handler handleRenderedTextClick assumes
e.target is an HTMLElement and calls closest(), which throws for Text nodes; add
a type guard at the top (e.g., const target = e.target; if (!(target instanceof
HTMLElement)) { this.openFile(item.file, item.lineNumber); return; }) so only
HTMLElements use closest() for internal-link, tag, and other link checks, and
non-HTMLElement targets immediately fall back to the default navigation via
openFile; ensure you still preventDefault() only when handling specific link
cases.
---
Outside diff comments:
In `@src/sphere-view.ts`:
- Around line 748-789: The click handler attached to item should ignore clicks
that originate from anchors so internal markdown links/tags remain clickable;
inside the item.addEventListener("click", ...) callback, check the event target
(e.g., const target = e.target as HTMLElement) and if target.closest("a") (or
target instanceof HTMLAnchorElement) return early, leaving link behavior
untouched, then continue using the existing finalLineResult, isOnFocus,
removeFromFocus and addToFocus logic as before.
| # Create Person Command | ||
|
|
||
| ## Summary | ||
|
|
||
| Add a "Create person" command to the Flow plugin so users can scaffold person notes via the command palette, mirroring the existing "Create new project" command pattern. | ||
|
|
||
| Prompted by [Discussion #35](https://github.com/tavva/flow/discussions/35). | ||
|
|
||
| ## Components | ||
|
|
||
| ### 1. Settings additions | ||
|
|
||
| - `personsFolderPath: string` (default `"People"`) — folder where person notes are created | ||
| - `personTemplateFilePath: string` (default `"Templates/Person.md"`) — path to person template file | ||
|
|
||
| Both configurable in the settings tab. | ||
|
|
||
| ### 2. Default person template | ||
|
|
||
| ```markdown | ||
| --- | ||
| creation-date: {{ date }}T{{ time }} | ||
| tags: person | ||
| --- | ||
|
|
||
| ## Discuss next | ||
| ``` | ||
|
|
||
| Template variables: `{{ date }}` (YYYY-MM-DD), `{{ time }}` (HH:MM:00) — same as the project template. | ||
|
|
||
| ### 3. NewPersonModal | ||
|
|
||
| A simple modal with: | ||
| - Name text input (required) | ||
| - Creates file at `{personsFolderPath}/{sanitizedName}.md` | ||
| - Reads template from `personTemplateFilePath`, falls back to hardcoded default if template file missing | ||
| - Replaces `{{ name }}`, `{{ date }}`, `{{ time }}` template variables | ||
| - Errors if file already exists | ||
| - Opens the file after creation | ||
|
|
||
| ### 4. Command registration | ||
|
|
||
| `addCommand` in `main.ts` with id `create-person`, name `Create person`, wired to open `NewPersonModal`. Same pattern as "Create new project". | ||
|
|
||
| ## What's excluded | ||
|
|
||
| - No sphere/priority/status — person notes don't use these |
There was a problem hiding this comment.
Add required project frontmatter and sections for markdown compliance.
This doc is missing the required YAML frontmatter and the Title/Description/Next actions sections expected for Flow project markdown files.
✅ Suggested structure
+---
+creation-date: 2026-02-16T00:00:00
+priority: 3
+tags:
+ - project/flow
+status: planning
+---
+
+# Create Person Command
+
+## Description
+Add a "Create person" command to the Flow plugin so users can scaffold person notes via the command palette.
+
+## Next actions
+- [ ] Draft implementation plan and validate with existing create-project flowAs per coding guidelines "Flow projects must follow the project structure with YAML frontmatter including creation-date, priority, tags (project/*), status, and optional parent-project, followed by title, description, and next actions sections".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-02-16-create-person-command-design.md` around lines 1 - 47,
The person template and modal logic must produce Flow-compliant markdown: update
the default template referenced by personTemplateFilePath and the NewPersonModal
creation flow to include YAML frontmatter with creation-date, priority, tags
(include project/person or project/* as appropriate), status, and optional
parent-project, plus the required Title, Description, and Next actions sections;
ensure NewPersonModal replaces {{name}}, {{date}}, {{time}} and also inserts a
Title line ("# {{name}}"), a Description paragraph placeholder, and a "## Next
actions" section, and add a preflight check in the create-person command flow to
validate/normalize frontmatter fields and error if the target file already
exists.
| --- | ||
|
|
||
| ### Task 1: Add settings fields | ||
|
|
||
| **Files:** |
There was a problem hiding this comment.
Fix heading level jump (MD001).
The document jumps from # to ###. Add a ## section heading before the Task headings (or promote Tasks to ##) to keep increments by one.
Suggested fix
---
+## Tasks
+
### Task 1: Add settings fields🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 13-13: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3
(MD001, heading-increment)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/2026-02-16-create-person-command.md` around lines 11 - 15, The
document jumps from a top-level heading to `###` which violates MD001; update
the heading hierarchy by adding a `##` section heading above the Task headings
or promote each `### Task 1: Add settings fields` (and subsequent Task headings)
to `##` so headings increment by one; locate and edit the heading lines in
docs/plans/2026-02-16-create-person-command.md (e.g., the "Task 1: Add settings
fields" heading) and change the markdown level accordingly.
| private handleRenderedTextClick(e: MouseEvent, item: FocusItem): void { | ||
| const target = e.target as HTMLElement; | ||
|
|
||
| // Handle clicks on internal links (wikilinks) | ||
| const internalLink = target.closest("a.internal-link") as HTMLElement | null; | ||
| if (internalLink) { | ||
| e.preventDefault(); | ||
| const href = internalLink.getAttribute("data-href"); | ||
| if (href) { | ||
| this.app.workspace.openLinkText(href, item.file); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // Handle clicks on tags | ||
| const tagLink = target.closest("a.tag") as HTMLElement | null; | ||
| if (tagLink) { | ||
| e.preventDefault(); | ||
| const tag = tagLink.textContent; | ||
| if (tag) { | ||
| const searchPlugin = (this.app as any).internalPlugins?.getPluginById?.("global-search"); | ||
| searchPlugin?.instance?.openGlobalSearch?.(`tag:${tag}`); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // Handle clicks on any other link (e.g. external URLs) | ||
| if (target.closest("a")) { | ||
| return; | ||
| } | ||
|
|
||
| // Default: navigate to source file | ||
| this.openFile(item.file, item.lineNumber); | ||
| } |
There was a problem hiding this comment.
Guard against non-HTMLElement targets in click handling.
e.target can be a Text node, which doesn’t have closest(). That will throw and break clicks on plain text. Add a type guard before using closest, and fall back to the default navigation path.
Suggested fix
private handleRenderedTextClick(e: MouseEvent, item: FocusItem): void {
- const target = e.target as HTMLElement;
+ const target = e.target;
+ if (!(target instanceof HTMLElement)) {
+ void this.openFile(item.file, item.lineNumber);
+ return;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private handleRenderedTextClick(e: MouseEvent, item: FocusItem): void { | |
| const target = e.target as HTMLElement; | |
| // Handle clicks on internal links (wikilinks) | |
| const internalLink = target.closest("a.internal-link") as HTMLElement | null; | |
| if (internalLink) { | |
| e.preventDefault(); | |
| const href = internalLink.getAttribute("data-href"); | |
| if (href) { | |
| this.app.workspace.openLinkText(href, item.file); | |
| } | |
| return; | |
| } | |
| // Handle clicks on tags | |
| const tagLink = target.closest("a.tag") as HTMLElement | null; | |
| if (tagLink) { | |
| e.preventDefault(); | |
| const tag = tagLink.textContent; | |
| if (tag) { | |
| const searchPlugin = (this.app as any).internalPlugins?.getPluginById?.("global-search"); | |
| searchPlugin?.instance?.openGlobalSearch?.(`tag:${tag}`); | |
| } | |
| return; | |
| } | |
| // Handle clicks on any other link (e.g. external URLs) | |
| if (target.closest("a")) { | |
| return; | |
| } | |
| // Default: navigate to source file | |
| this.openFile(item.file, item.lineNumber); | |
| } | |
| private handleRenderedTextClick(e: MouseEvent, item: FocusItem): void { | |
| const target = e.target; | |
| if (!(target instanceof HTMLElement)) { | |
| void this.openFile(item.file, item.lineNumber); | |
| return; | |
| } | |
| // Handle clicks on internal links (wikilinks) | |
| const internalLink = target.closest("a.internal-link") as HTMLElement | null; | |
| if (internalLink) { | |
| e.preventDefault(); | |
| const href = internalLink.getAttribute("data-href"); | |
| if (href) { | |
| this.app.workspace.openLinkText(href, item.file); | |
| } | |
| return; | |
| } | |
| // Handle clicks on tags | |
| const tagLink = target.closest("a.tag") as HTMLElement | null; | |
| if (tagLink) { | |
| e.preventDefault(); | |
| const tag = tagLink.textContent; | |
| if (tag) { | |
| const searchPlugin = (this.app as any).internalPlugins?.getPluginById?.("global-search"); | |
| searchPlugin?.instance?.openGlobalSearch?.(`tag:${tag}`); | |
| } | |
| return; | |
| } | |
| // Handle clicks on any other link (e.g. external URLs) | |
| if (target.closest("a")) { | |
| return; | |
| } | |
| // Default: navigate to source file | |
| this.openFile(item.file, item.lineNumber); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/focus-view.ts` around lines 910 - 943, The click handler
handleRenderedTextClick assumes e.target is an HTMLElement and calls closest(),
which throws for Text nodes; add a type guard at the top (e.g., const target =
e.target; if (!(target instanceof HTMLElement)) { this.openFile(item.file,
item.lineNumber); return; }) so only HTMLElements use closest() for
internal-link, tag, and other link checks, and non-HTMLElement targets
immediately fall back to the default navigation via openFile; ensure you still
preventDefault() only when handling specific link cases.
e.target can be a Text node when clicking plain text inside rendered markdown. Text nodes don't have closest(), which would throw. Add an instanceof check and fall back to source file navigation.
Summary
MarkdownRenderer.renderMarkdown()instead of plainsetText(), making[[wikilinks]]and#tagsclickablehandleRenderedTextClickmethod across all three item render paths (unpinned, pinned, completed)Addresses item 3 of 4 from @DanValnicek's feedback in #37.
Test plan
Summary by CodeRabbit
Release Notes
New Features
Settings