Skip to content

Make wikilinks and tags clickable in focus view#46

Merged
tavva merged 28 commits intobetafrom
feature/clickable-focus-links
Feb 19, 2026
Merged

Make wikilinks and tags clickable in focus view#46
tavva merged 28 commits intobetafrom
feature/clickable-focus-links

Conversation

@tavva
Copy link
Owner

@tavva tavva commented Feb 19, 2026

Summary

  • Render focus action text with MarkdownRenderer.renderMarkdown() instead of plain setText(), making [[wikilinks]] and #tags clickable
  • Add explicit click handlers for internal links (opens linked note) and tags (opens Obsidian tag search), since MarkdownRenderer doesn't set these up in custom ItemView contexts
  • Extract shared handleRenderedTextClick method across all three item render paths (unpinned, pinned, completed)

Addresses item 3 of 4 from @DanValnicek's feedback in #37.

Test plan

  • Wikilinks in focus items open the referenced note when clicked
  • Tags in focus items open Obsidian's search with the tag query
  • Clicking non-link text still navigates to the action's source file
  • All 890 tests pass, build clean

Summary by CodeRabbit

Release Notes

  • New Features

    • Added person note creation with template support and customizable folder paths
    • Introduced visual indicators for waiting-for actions and event tracking for action state changes
    • Enhanced focus highlighting with dynamic UI updates that reduce unnecessary page refreshes
    • Refactored system to support GTD features including sphere hierarchies, focus lists, and waiting-for tracking
  • Settings

    • Added People Folder and Person Template File configuration options with support for variable substitution

tavva added 27 commits February 16, 2026 05:35
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.
@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/clickable-focus-links

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Prevent 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.

Comment on lines +1 to +47
# 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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 flow

As 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.

Comment on lines +11 to +15
---

### Task 1: Add settings fields

**Files:**
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +910 to +943
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);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.
@tavva tavva merged commit 1cb4d7e into beta Feb 19, 2026
3 checks passed
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