m4.5: titles optional — every note gets an auto display name
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 33s

Capture starts in the body, so forcing a title feels odd and body-only
notes had no name — which made them unlinkable. Fix both: persist a
display_title = explicit title if set, else the note's first non-empty
body line (deterministic, no AI). The title field stays optional.

- migration 0012: notes.display_title (NOT NULL, best-effort backfill;
  the app recomputes precisely on next save)
- derive_display_title() helper, set on create + update
- drive the /titles index, backlinks, graph edges + node labels, and
  [[wiki-link]] resolution off display_title so body-only notes are
  nameable, findable (command palette / [[ autocomplete), and linkable
- rename-repoint generalized: inbound [[Old Name]] links now survive a
  name change via the first body line too, not just an explicit title
- unit tests for the derivation (explicit wins, first non-empty line,
  blank/empty, length cap)
- frontend: display_title on the Note type; title field placeholder now
  reads "Title (optional)"

First item of M4.5 (frictionless input & recall); unblocks the linking
work. Card rendering unchanged (no first-line duplication).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-22 08:00:01 -04:00
co-authored by Claude Opus 4.8
parent 1b6eb0053b
commit 2b6a353666
7 changed files with 122 additions and 25 deletions
+33 -1
View File
@@ -2,7 +2,13 @@ import pytest
from thoughtsync.app import create_app
from thoughtsync.models.note import NOTE_COLORS, Note
from thoughtsync.notes import is_empty_note, normalize_color, parse_link_titles, rewrite_link_title
from thoughtsync.notes import (
derive_display_title,
is_empty_note,
normalize_color,
parse_link_titles,
rewrite_link_title,
)
@pytest.fixture
@@ -97,6 +103,32 @@ def test_rewrite_link_title_noop():
assert rewrite_link_title("no links here", "alpha", "Gamma") == "no links here"
def test_derive_display_title_explicit_wins():
assert derive_display_title("My Title", "some body line") == "My Title"
assert derive_display_title(" Padded ", "body") == "Padded"
def test_derive_display_title_from_first_body_line():
assert derive_display_title(None, "first line\nsecond line") == "first line"
assert derive_display_title("", " spaced first \nnext") == "spaced first"
# leading blank/whitespace lines are skipped to the first line with content
assert derive_display_title(None, "\n \nreal line\nmore") == "real line"
# a whitespace-only title falls through to the body
assert derive_display_title(" ", "body wins") == "body wins"
def test_derive_display_title_empty():
assert derive_display_title(None, None) == ""
assert derive_display_title("", "") == ""
assert derive_display_title(" ", " \n ") == ""
def test_derive_display_title_caps_length():
long = "x" * 300
assert derive_display_title(None, long) == "x" * 200
assert derive_display_title(long, "body") == "x" * 200
async def test_titles_requires_auth(app):
client = app.test_client()
resp = await client.get("/api/notes/titles")