Repository navigation
Bug: Placeholder node loses focus immediately after typing, preventing text input #528
Description
Bug Description
When attempting to type in a placeholder node (empty text node in date viewer), the node loses focus immediately after pressing the first key, preventing any text input. The textarea element is destroyed and the component switches back to view mode before the input can be registered.
Reproduction Steps
- Start the dev server:
bun run dev:browser - Open http://localhost:5173 in Chrome
- Navigate to a date page (e.g., "Yesterday")
- Click on the empty placeholder node (should show as a clickable area)
- Observe that the placeholder switches to edit mode (textarea appears)
- Press any key (e.g., "T", "H", etc.)
- BUG: The textarea immediately loses focus and switches back to view mode
- No text is entered
Expected Behavior
- Pressing a key should insert that character into the textarea
- The textarea should remain focused and in edit mode
- User should be able to continue typing
Actual Behavior
- The textarea loses focus immediately after the first keypress
- The component switches back to view mode (placeholder div)
- No text is entered
- The cycle repeats if you click again
Root Cause Analysis
Investigation Timeline
Using Chrome DevTools MCP integration, I traced the focus events:
// Focus event sequence when typing "H":
1. DIV#view__pane-1__xxx gets FOCUS (click on placeholder)
2. DIV gets BLUR immediately (switching to edit mode)
3. TEXTAREA#textarea__pane-1__xxx gets FOCUS (edit mode active)
4. User presses "H" key
5. KEYDOWN event fires on textarea
6. TEXTAREA gets BLUR (element being destroyed!)
7. activeElement becomes BODY (nothing focused)Detailed Timing Analysis
Debug logs show the exact moment of destruction:
[DEBUG] ===== KEYDOWN on TEXTAREA =====
[DEBUG] Key: H
[DEBUG] Document activeElement BEFORE: textarea__pane-1__e573969b-081a-40ae-b330-b7a1ff19ea51
[DEBUG] Document activeElement AFTER microtask: textarea__pane-1__e573969b-081a-40ae-b330-b7a1ff19ea51 ✅
[DEBUG] Document activeElement AFTER 0ms timeout: ❌ (EMPTY!)
[DEBUG] Textarea still exists? false ❌ (DESTROYED!)Key Finding: The textarea is destroyed between the microtask queue and the next event loop tick (setTimeout 0ms). This is a Svelte reactivity issue.
Why This Happens
- Keydown event fires on the textarea element
- Somewhere in the event handling chain, a reactive state change occurs
- Svelte schedules a re-render (happens in the next microtask/tick)
- The re-render switches
isEditingfromtruetofalse - The textarea is unmounted and replaced with the view-mode div
- The blur event fires (natural consequence of element removal)
- The blur handler calls
focusManager.clearEditing()(line 648 in base-node.svelte) - This confirms
editingNodeId = null, keeping the component in view mode
Technical Investigation Findings
Key Files Involved
-
packages/desktop-app/src/lib/design/components/base-node.svelte- Lines 83-85:
isEditingis derived fromfocusManager.editingNodeId - Lines 640-659:
blur()event handler callsfocusManager.clearEditing()
- Lines 83-85:
-
packages/desktop-app/src/lib/design/components/textarea-controller.ts- Lines 589-627:
handleKeyDown()method processes keyboard events - No obvious code that would trigger blur/unmount
- Lines 589-627:
-
packages/desktop-app/src/lib/services/focus-manager.svelte.ts- Lines 316-323:
clearEditing()setseditingNodeId = null - This is called from base-node blur handler
- Lines 316-323:
The Problematic Code
File: packages/desktop-app/src/lib/design/components/base-node.svelte:640-659
blur: () => {
// Use FocusManager as single source of truth
// Only clear editing if THIS node is still the editing node
// (Don't clear if focus has already moved to another node via arrow navigation)
// CRITICAL: Don't clear if this is a node type conversion (component is re-mounting)
untrack(() => {
const isNodeTypeConversion = focusManager.cursorPosition?.type === 'node-type-conversion';
if (focusManager.editingNodeId === nodeId && !isNodeTypeConversion) {
focusManager.clearEditing(); // ⚠️ THIS IS THE PROBLEM
}
});
// ... rest of blur handler
}The Issue: The blur handler cannot distinguish between:
- Intentional blur (user clicked away, pressed Escape, etc.) ✅ Should clear editing
- Unintentional blur (element being destroyed during re-render) ❌ Should NOT clear editing
Why is the Textarea Being Destroyed?
The textarea is destroyed because isEditing becomes false:
// Line 83-85
let isEditing = $derived(
focusManager.editingNodeId === nodeId && focusManager.editingPaneId === paneId
);When focusManager.editingNodeId becomes null (from clearEditing()), isEditing becomes false, which triggers the component to render the view mode div instead of the textarea.
But what's triggering the initial re-render before the blur? This needs investigation.
Investigation Steps for Junior Engineer
Step 1: Verify the Bug Locally
- Follow the reproduction steps above
- Open Chrome DevTools Console
- Try typing in the placeholder - confirm it loses focus
Step 2: Add Debug Logging
Add debug logs to track the state changes:
In base-node.svelte, add logging to the blur handler (line 640):
blur: () => {
console.log('[BLUR-DEBUG] Blur fired', {
nodeId,
editingNodeId: focusManager.editingNodeId,
isNodeTypeConversion: focusManager.cursorPosition?.type === 'node-type-conversion',
activeElement: document.activeElement?.id,
callStack: new Error().stack
});
untrack(() => {
const isNodeTypeConversion = focusManager.cursorPosition?.type === 'node-type-conversion';
if (focusManager.editingNodeId === nodeId && !isNodeTypeConversion) {
console.log('[BLUR-DEBUG] Calling clearEditing()');
focusManager.clearEditing();
}
});
// ...
}In focus-manager.svelte.ts, add logging to clearEditing() (line 316):
clearEditing(): void {
console.log('[FOCUS-DEBUG] clearEditing() called', {
previousEditingNodeId: _editingNodeId,
callStack: new Error().stack
});
_editingNodeId = null;
_cursorPosition = null;
// ...
}Step 3: Track What Triggers the Re-render
Add logging to track isEditing state changes:
In base-node.svelte, add an effect to monitor isEditing:
$effect(() => {
console.log('[EDITING-DEBUG] isEditing changed', {
nodeId,
isEditing,
editingNodeId: focusManager.editingNodeId,
editingPaneId: focusManager.editingPaneId,
paneId
});
});Step 4: Identify the Trigger
Run the app with these debug logs and:
- Click the placeholder
- Press a key
- Look at the console logs to see:
- What's the sequence of events?
- Is
isEditingchanging fromtruetofalseBEFORE the blur? - What's calling
clearEditing()? - Is there any other state change triggering a re-render?
Step 5: Check for Reactive State Changes During Keydown
Look for any state changes in handleKeyDown() or related code that might trigger a re-render:
Check these files:
textarea-controller.ts:589-627-handleKeyDown()method- Any keyboard command handlers in
packages/desktop-app/src/lib/commands/keyboard/ - Pattern detection code (called during input handling)
Look for:
- Svelte store updates (
$someStore.set(),someStore.update()) - Reactive state assignments that might trigger re-renders
- Any code that modifies
focusManagerstate during keydown
Potential Fix Approaches
Option 1: Debounce the blur handler
Don't immediately clear editing on blur - wait to see if focus returns:
blur: () => {
let blurTimeout;
// Wait a tick to see if this is just a temporary blur during re-render
setTimeout(() => {
untrack(() => {
const isNodeTypeConversion = focusManager.cursorPosition?.type === 'node-type-conversion';
// Only clear if we're still blurred and no other textarea has focus
if (
focusManager.editingNodeId === nodeId &&
!isNodeTypeConversion &&
document.activeElement?.tagName !== 'TEXTAREA'
) {
focusManager.clearEditing();
}
});
}, 10); // Small delay to let re-render complete
}Option 2: Check if blur is due to element removal
Add a flag to track if the component is being unmounted:
let isUnmounting = false;
onDestroy(() => {
isUnmounting = true;
});
// In blur handler:
blur: () => {
if (isUnmounting) {
return; // Don't clear editing if component is being destroyed
}
// ... rest of blur logic
}Option 3: Prevent the re-render trigger (root cause fix)
Find what's causing isEditing to become false during keydown and fix that:
- Identify the reactive state change during keydown (Step 5 above)
- Prevent that state change from happening prematurely
- Or defer the state change until after input is processed
Option 4: Add a "keydownInProgress" flag
Track when a keydown is being processed and don't clear editing during that time:
// In focus-manager:
let _keydownInProgress = false;
clearEditing(): void {
if (_keydownInProgress) {
console.log('[FOCUS-DEBUG] Ignoring clearEditing during keydown');
return;
}
_editingNodeId = null;
// ...
}// In textarea-controller handleKeyDown:
private async handleKeyDown(event: KeyboardEvent): Promise<void> {
focusManager._keydownInProgress = true;
try {
// ... existing keydown logic
} finally {
setTimeout(() => {
focusManager._keydownInProgress = false;
}, 0);
}
}Acceptance Criteria
- Clicking on placeholder node switches to edit mode
- Pressing any key inserts that character into the textarea
- Textarea remains focused after keypress
- User can continue typing without re-clicking
- No console errors or warnings
- Blur still works correctly when clicking outside the node
- No regression in existing focus/blur behavior
Additional Context
This bug was discovered during UX testing with Chrome DevTools MCP integration. The investigation revealed that:
- The focus event sequence is correct up until the keydown
- The textarea exists and has focus when keydown fires
- The textarea is destroyed between the microtask and the next tick
- The blur event is a consequence, not the cause
- The root issue is likely a reactive state change triggering a premature re-render
Related Files
packages/desktop-app/src/lib/design/components/base-node.svelte(lines 83-85, 640-659)packages/desktop-app/src/lib/design/components/textarea-controller.ts(lines 589-627)packages/desktop-app/src/lib/services/focus-manager.svelte.ts(lines 316-323)packages/desktop-app/src/lib/components/viewers/date-node-viewer.svelte(placeholder rendering)
Environment
- Browser: Chrome (tested with DevTools MCP)
- Dev server:
bun run dev:browser - Node version: Latest
- Bun version: Latest
Activity
malibio commentedon Nov 17, 2025
Test Baseline
Running test suite before implementation:
Results:
Pre-existing failure (unrelated to this issue):
http-adapter-surrealdb.test.ts > Error Handling > should throw error on update with non-existent nodeThis baseline will be used to verify no new test failures are introduced by the fix.
Fix: Placeholder node loses focus after typing (Issue #528)
malibio commentedon Nov 17, 2025
Testing Update
Good News: The typing bug is FIXED! ✅
Manual testing with Chrome DevTools confirmed:
Discovered Issue: While testing, found that typed content is not persisting to the database. However, this appears to be a separate issue unrelated to the focus/typing bug:
This persistence issue likely affects all node creation, not just placeholders. Will create a separate issue for investigation.
Fix: Complete parent-child edge creation for graph-native architectur…
malibio commentedon Nov 17, 2025
✅ COMPLETE FIX VERIFIED AND COMMITTED
Both the typing bug AND the underlying parent-child edge creation issue are now fixed!
What Was Actually Broken
Your instinct was 100% correct - the / removal in PR #523 caused this.
The bug was more severe than initially thought - it wasn't just the frontend cache, it was complete failure of parent-child edge creation in the backend.
The Two-Part Bug
Part 1: Frontend Cache (first commit)
Part 2: Backend Edge Creation (second commit - THIS WAS THE REAL ISSUE)
The Fix
_parentId/_containerIdto promoted nodesVerification ✅
Manual Testing:
node:2630b0a2-2c34-4e71-81f4-67be32c8df69has_child:o8b8a2msmg6tktqjq9smnode:2025-11-16(date container)node:2630b0a2-2c34-4e71-81f4-67be32c8df69Test Status:
PR Updated
PR #529 now contains the complete fix:
This was a critical architectural bug that affected all node creation, not just placeholders!
malibio commentedon Nov 17, 2025
Implementation Update - Two Fixes in One PR
Reviewer Feedback: The PR was questioned for not matching the issue description (focus on typing/focus loss vs. parent-child edge creation).
Clarification: During investigation, I discovered the focus loss was actually a symptom of a deeper bug:
Root Cause Chain
parentId/containerNodeIdfrom the HTTP APIgetNodesForParent()The Two-Part Fix
Commit 1 (f49f67d): Fixed the immediate symptom
Commit 2 (175862c): Fixed the root cause
parentId/containerNodeIdas operation parameters (not stored fields)Why Both Were Needed
Without commit 1: Focus loss continues (cache not updated)
Without commit 2: Edges don't persist, nodes orphaned on page reload
This is a single coherent fix for what appeared to be a focus issue but was actually a graph architecture regression introduced in PR #523.
Test Results:
Responding to code review feedback with additional tests and documentation.
Address code review feedback (Issue #528)
malibio commentedon Nov 17, 2025
✅ Issue #528 Resolved and Merged
PR #529 has been successfully merged to main! 🎉
What Was Fixed
This issue revealed a complex chain of problems introduced by PR #523's graph migration:
Solution Implemented
Two-part fix addressing both backend and frontend:
Backend (Commit 175862c):
move_node()operationFrontend (Commit f49f67d):
registerChildWithParent()to prevent duplicationQuality Assurance
Tests Added:
Documentation:
Code Review:
Test Baseline Verified
Before PR: 39 failed, 1551 passed
After PR: 39 failed, 1552 passed
Result: NO NEW FAILURES ✅
Related Work
This fix is part of the larger graph-native architecture migration:
Status: Done
Merged: PR #529 (squash merge)
Branch:
feature/issue-528-fix-placeholder-typing-bug(deleted)Commits: 5 commits squashed into main
Complete hierarchy migration: remove container concept and use only p…
Complete Hierarchy Migration: Remove Container Concept and Fix All Pa…
1 remaining item
Remove dead workaround code from node-hierarchy.ts (#617)
Remove dead workaround code from node-hierarchy.ts (#617)
Refactor tree loading to use efficient adjacency list strategy (#632)
Bug: Placeholder node loses focus immediately after typing, preventin…
Complete Hierarchy Migration: Remove Container Concept and Fix All Pa…
Remove dead workaround code from node-hierarchy.ts (#617)
Refactor tree loading to use efficient adjacency list strategy (#632)
Bug: Placeholder node loses focus immediately after typing, preventin…
Complete Hierarchy Migration: Remove Container Concept and Fix All Pa…
Remove dead workaround code from node-hierarchy.ts (#617)
Refactor tree loading to use efficient adjacency list strategy (#632)
Bug: Placeholder node loses focus immediately after typing, preventin…
Complete Hierarchy Migration: Remove Container Concept and Fix All Pa…
Remove dead workaround code from node-hierarchy.ts (#617)
Refactor tree loading to use efficient adjacency list strategy (#632)