Files
logseq/docs/agent-guide/logseq-cli/009-show-page-hierarchy.md
2026-05-08 21:36:22 +08:00

27 KiB

Show Normal Page Hierarchy Implementation Plan

Goal: Add a logseq show option named --page-hierarchy so an ordinary page can display its child page hierarchy when requested, while preserving the current ordinary page content-tree behavior by default.

Architecture: Keep the behavior inside the existing CLI show command namespace. Architecture: Reuse the current db-worker-node transport APIs, especially :thread-api/pull and :thread-api/q; do not add a new thread API unless concrete implementation work proves the existing query/pull path cannot fetch the required page hierarchy. Architecture: Build on the existing show --page Library implementation, which already renders page hierarchy data through :block/parent and the shared show tree renderer. Architecture: Keep --page-hierarchy defaulting to false so normal show --page <page> output remains unchanged unless the option is explicitly enabled.

Tech Stack: ClojureScript, Logseq CLI show, Datascript queries through db-worker-node, page hierarchy stored in :block/parent, logseq.db entity predicates, Promesa, CLI unit tests, command parser tests, CLI formatter/structured-output tests, and CLI e2e manifests.

Related: Builds on /Users/rcmerci/gh-repos/logseq/docs/agent-guide/logseq-cli/008-show-library-page.md, the current implementation in /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs, and the db-worker-node thread APIs in /Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs.

Problem statement

The current CLI show command has two page-related display modes:

  1. Ordinary pages render their content blocks by querying blocks whose :block/page is the selected page id.
  2. The built-in Library page is special-cased to render child page entities whose :block/parent points at Library, recursively.

This is correct for default show --page <page> behavior because users expect page content blocks by default.

However, Logseq DB graphs can also represent page hierarchy through page entities connected by :block/parent. Namespace page creation such as foo/bar/baz creates page parent relationships, and the Desktop App can show page breadcrumb/hierarchy relationships based on that parent chain.

The CLI currently has no explicit way to ask an ordinary page target to render its child page hierarchy instead of its content blocks.

The requested feature is to add a --page-hierarchy option to show:

logseq show --page Foo --page-hierarchy true

When the option is true and the selected target is an ordinary page, show should display the selected page as the root and recursively display child pages whose :block/parent points at that page.

When the option is omitted or false, show --page Foo must continue to display page content blocks exactly as it does today.

After implementation, review the change with logseq-review-workflow before considering the work complete.

Current implementation snapshot

The current show command lives in:

/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs

The current option spec is show-spec, which already contains boolean options such as:

:linked-references
:ref-id-footer

The current action construction path is:

show-spec
  -> core/command-entry
  -> commands/build-action
  -> show-command/build-action

show-command/build-action currently stores normalized options in the action map, including:

:linked-references?
:ref-id-footer?
:uuid
:page
:level

The current page fetch path is:

fetch-tree
  -> pull selected entity with show-root-selector
  -> attach-user-properties-to-entity
  -> fetch-tree-for-entity

fetch-tree-for-entity currently routes entities like this:

if built-in Library page
  -> fetch-library-tree-for-entity
else if entity has [:block/page :db/id]
  -> block subtree path
else if entity has :db/id
  -> ordinary page content path using fetch-blocks-for-page
else
  -> not found error

The ordinary page content path uses:

fetch-blocks-for-page
  -> :thread-api/q
  -> query blocks by [?b :block/page ?page-id]
  -> remove property value blocks
  -> attach user properties
  -> build-tree grouped by [:block/parent :db/id]

The Library page hierarchy path uses:

fetch-library-tree-for-entity
  -> fetch-library-children
  -> :thread-api/q
  -> query children by [?child :block/parent ?parent-id]
  -> filter ordinary display pages
  -> sort by :block/order
  -> recurse with cycle protection

The existing Library implementation proves that the current db-worker-node transport is sufficient for this feature:

:thread-api/pull
  -> fetch selected page/block entities

:thread-api/q
  -> fetch page hierarchy children by :block/parent

:thread-api/get-block-refs
  -> existing linked-reference behavior

:thread-api/get-block-parents
  -> existing breadcrumb behavior for ordinary block roots

Therefore, the first implementation should not add any new thread API.

Page hierarchy model

Ordinary page hierarchy is stored with :block/parent links between page entities.

Examples from the current codebase:

  • /Users/rcmerci/gh-repos/logseq/deps/outliner/src/logseq/outliner/page.cljs can create namespace pages with parent relationships when :split-namespace? true.
  • /Users/rcmerci/gh-repos/logseq/deps/outliner/test/logseq/outliner/page_test.cljs verifies namespace pages such as foo/bar/baz produce foo -> bar -> baz parent relationships.
  • /Users/rcmerci/gh-repos/logseq/deps/db/src/logseq/db/frontend/db.cljs exposes get-page-parents, which walks :block/parent upward for page entities.
  • The existing CLI Library path walks the inverse direction by querying children where :block/parent equals the current page id.

The new CLI option should use the same inverse child query as the Library path for ordinary pages.

Desired CLI behavior

Default behavior remains unchanged

These commands must continue to render ordinary page content blocks:

logseq show --page Foo
logseq show --page Foo --page-hierarchy false

For ordinary pages, the default path must keep querying :block/page content blocks and must not query :block/parent page children just because page hierarchy exists.

Explicit page hierarchy behavior

This command should render page hierarchy:

logseq show --page Foo --page-hierarchy true

Expected tree shape:

Foo
└── Bar
    └── Baz

Where:

Bar has :block/parent Foo
Baz has :block/parent Bar

The root row should be the selected page entity.

Child rows should be child page entities whose :block/parent points at the selected page.

Nested rows should be child page entities whose :block/parent points at another page already displayed in the hierarchy.

Children should be sorted by :block/order, matching the existing block tree and Library page hierarchy behavior.

The hierarchy display should not inline ordinary content blocks from child pages. If Bar has normal content blocks whose :block/page is Bar, those blocks should not appear in show --page Foo --page-hierarchy true.

Filtering

Use the same display filter as the current Library implementation unless implementation research proves normal page hierarchy needs a different Desktop semantic:

show ordinary page entities
hide class pages
hide property pages
hide non-page blocks

This matches the current library-display-page? behavior:

ldb/page?
not ldb/class?
not ldb/property?

The implementation should avoid showing arbitrary non-page blocks that happen to have :block/parent pointing at a page.

--level

--level should limit page hierarchy depth exactly like it limits existing show trees.

Use the existing convention:

root depth = 1
children are omitted when depth >= max-depth

Example:

logseq show --page Foo --page-hierarchy true --level 2

Should show Foo -> Bar but omit Baz in Foo -> Bar -> Baz.

Library compatibility

logseq show --page Library should keep the existing special Library behavior without requiring --page-hierarchy true.

The --page-hierarchy flag should not break or replace Library behavior.

These commands should both render the Library page hierarchy:

logseq show --page Library
logseq show --page Library --page-hierarchy true

The Library branch should remain special because it is current shipped behavior and because the feature request specifically asks to reference show --page Library, not to make Library require the new flag.

Target scope

The option is primarily for page targets.

Recommended behavior:

  • If the selected entity is a page and :page-hierarchy? is true, render page hierarchy.
  • If the selected entity is the built-in Library page, render Library hierarchy regardless of the flag.
  • If the selected entity is an ordinary block, keep the current block subtree behavior even if the flag is true.

This allows --id <page-id> --page-hierarchy true and --uuid <page-uuid> --page-hierarchy true to behave consistently when the id or UUID selects a page, while avoiding surprising changes for block targets.

If implementation discovers that supporting --id/--uuid page targets adds ambiguity or broadens the contract too much, narrow the first implementation to --page targets only and add tests documenting that scope. Prefer consistency for page entities if it remains simple.

Linked references and UUID references

Linked references should keep the current behavior.

If linked references are enabled, show should fetch linked references for the rendered root id, just as it does today.

UUID reference collection should continue to run over the root tree and linked references after page hierarchy construction.

No page-hierarchy-specific linked-reference or UUID-reference code should be necessary if the page hierarchy tree is represented as ordinary :root + :block/children data.

Structured output

--output json and --output edn should expose the page hierarchy under the existing structured output shape:

[:data :root :block/children]

Do not add a second top-level hierarchy field.

Do not add human-only labels such as Page hierarchy to structured output.

If implementation adds internal traversal metadata, use :show/* keys so strip-show-internal-data removes it automatically.

Structured output should continue to remove :block/uuid through the existing sanitize-structured-tree path.

Keep the implementation in:

/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs

Do not create a new command.

Do not add a new db-worker-node thread API for the first implementation.

Use existing worker transport calls:

:thread-api/pull
:thread-api/q
:thread-api/get-block-refs
:thread-api/get-block-parents

Add the CLI option

Add :page-hierarchy to show-spec:

:page-hierarchy {:desc "Show child page hierarchy for page targets (default false)"
                 :coerce :boolean}

Add the value to the action map in build-action:

:page-hierarchy? (if (contains? options :page-hierarchy)
                   (:page-hierarchy options)
                   false)

Because false is the default, existing tests and behavior should remain stable.

Update entries examples only if helpful. A concise example is enough:

logseq show --graph my-graph --page Foo --page-hierarchy true

Generalize Library hierarchy helpers

The current Library helper names are Library-specific:

library-child-selector
library-display-page?
fetch-library-children
fetch-library-tree-for-entity

The behavior is now useful for both Library and ordinary page hierarchy.

Recommended refactor:

library-child-selector
  -> page-hierarchy-child-selector

library-display-page?
  -> page-hierarchy-display-page?

fetch-library-children
  -> fetch-page-hierarchy-children

fetch-library-tree-for-entity
  -> fetch-page-hierarchy-tree-for-entity

Keep library-page? as the Library detection predicate.

After refactoring, fetch-page-hierarchy-tree-for-entity should still be used by the Library branch.

This avoids duplicating the recursive hierarchy traversal and ensures future fixes apply to both Library and ordinary page hierarchy.

Preserve the Library contract

Route Library before ordinary page behavior, as it works today:

if library-page? entity
  fetch-page-hierarchy-tree-for-entity

Do not require :page-hierarchy? true for the Library page.

Do not treat arbitrary pages titled Library as built-in Library unless ldb/library? returns true.

Add an ordinary page hierarchy route

fetch-tree-for-entity currently receives only:

config repo entity max-depth

It needs access to the action option.

Recommended options:

  1. Add a page-hierarchy? argument:
fetch-tree-for-entity config repo entity max-depth page-hierarchy?
  1. Or pass a small context map:
fetch-tree-for-entity config repo entity {:max-depth max-depth
                                          :page-hierarchy? page-hierarchy?}

The context-map approach is easier to extend but touches more call sites. The extra-argument approach is simpler and likely enough.

Then route page entities like this:

fetch-tree-for-entity config repo entity max-depth page-hierarchy?
  if library-page? entity
    fetch-page-hierarchy-tree-for-entity
  else if page-hierarchy? and ordinary page entity
    fetch-page-hierarchy-tree-for-entity
  else if entity has [:block/page :db/id]
    existing block subtree path
  else if entity has :db/id
    existing normal page content path

Define a clear page predicate for the new branch.

A page entity in this command is typically an entity without [:block/page :db/id] and with ldb/page? true. Prefer using ldb/page? because selectors already include :block/tags with :db/ident for tag predicates.

Avoid enabling page hierarchy for ordinary content blocks.

Keep selectors shallow

The current show-root-selector already includes page tags, parent, order, title, name, UUID, built-in marker, status, and link fields.

The current library-child-selector includes the fields needed to render page rows and evaluate predicates.

When generalizing it to page-hierarchy-child-selector, keep it shallow and avoid recursive Datascript pull selectors.

The child selector should include at least:

:db/id
:db/ident
:block/name
:block/uuid
:block/title
:block/order
:logseq.property/built-in?
{:logseq.property/status [:db/ident :block/title]}
{:block/parent [:db/id :block/name :block/title :block/uuid]}
{:block/tags [:db/id :db/ident :block/name :block/title :block/uuid]}
{:block/link link-target-selector}

Do not fetch normal page content blocks for page hierarchy mode.

Fetching page hierarchy children

Use the same query shape as Library currently uses:

[:find (pull ?child page-hierarchy-child-selector)
 :in $ ?parent-id
 :where [?child :block/parent ?parent-id]]

Then:

  1. map first over rows.
  2. Filter with page-hierarchy-display-page?.
  3. Sort by :block/order.
  4. Attach user properties with attach-user-properties if hierarchy rows should preserve the existing CLI row-property rendering behavior.

Use attach-user-properties for consistency with Library and ordinary page rows.

Cycle protection

Keep the existing Library cycle protection and apply it to ordinary page hierarchy too.

If a malformed graph creates a :block/parent cycle, fail fast instead of recursing indefinitely.

The existing error code is:

:library-parent-cycle

When generalizing the helper, rename the error to a broader code if tests can be updated cleanly:

:page-hierarchy-parent-cycle

If preserving the existing :library-parent-cycle test is preferable to minimize churn, keep the old code but document that it also protects general page hierarchy. The cleaner long-term choice is to rename and update Library tests.

Linked block interaction

The current linked-block resolver calls fetch-tree-for-entity for linked targets.

After fetch-tree-for-entity gains a page-hierarchy? argument, pass the current action option through linked resolution.

Recommended behavior:

  • If the user runs show --page Foo --page-hierarchy true and a displayed hierarchy row links to another page, linked target resolution should use the same page-hierarchy? setting for page targets.
  • If this creates confusing output or complicates tests, keep linked target behavior unchanged and document that --page-hierarchy only changes the initially selected page target.

Prefer the simpler invariant: fetch-tree-for-entity always receives the same page-hierarchy? option for the whole show operation.

Add a focused linked-block test only if linked blocks are affected by the implementation.

Breadcrumb interaction

attach-breadcrumb-line currently adds breadcrumbs only for ordinary block roots through fetch-breadcrumb-parents and ordinary-block-root?.

Page hierarchy roots are pages, not ordinary blocks, so no new breadcrumb line is expected.

Do not add a new breadcrumb display in this feature unless a test demonstrates an existing regression.

Documentation

Update CLI documentation only if operator-facing docs already list show options or examples.

Candidate file:

/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md

Keep docs concise:

Use `--page-hierarchy true` with `show --page <page>` to display child pages connected through page hierarchy instead of normal page content blocks.

Do not add translation keys; this is CLI help/docs and existing CLI option descriptions, not shipped UI text in the renderer.

Implementation steps

  1. Add parser/action support.

    • Add :page-hierarchy to show-spec with :coerce :boolean.
    • Add :page-hierarchy? to the action map with default false.
    • Add or update command examples only if helpful.
  2. Refactor Library hierarchy helpers into generic page hierarchy helpers.

    • Rename library-child-selector to page-hierarchy-child-selector.
    • Rename library-display-page? to page-hierarchy-display-page?.
    • Rename fetch-library-children to fetch-page-hierarchy-children.
    • Rename fetch-library-tree-for-entity to fetch-page-hierarchy-tree-for-entity.
    • Keep library-page? for built-in Library detection.
  3. Generalize cycle protection.

    • Prefer a generic :page-hierarchy-parent-cycle error code.
    • Update Library cycle tests if the error code is renamed.
    • Ensure malformed page parent cycles fail fast for both Library and normal pages.
  4. Route ordinary page hierarchy mode.

    • Pass :page-hierarchy? from the action into fetch-tree, fetch-tree-for-entity, and linked target resolution as needed.
    • In fetch-tree-for-entity, route library-page? first.
    • If :page-hierarchy? is true and the selected entity is a normal page, call fetch-page-hierarchy-tree-for-entity.
    • Keep block targets and default normal page content unchanged.
  5. Preserve downstream rendering and structured output.

    • Return tree data as the existing root map with :block/children.
    • Avoid adding a new output section.
    • Keep resolve-linked-blocks-in-tree-data, linked references, UUID references, property title resolution, and structured sanitization on the existing pipeline.
  6. Update docs if needed.

    • Add a concise show --page <page> --page-hierarchy true example to CLI docs if the show section already documents options/examples.
  7. Run logseq-review-workflow after implementation and tests.

    • Apply repository-wide rules.
    • Apply Logseq CLI module rules.
    • Apply Datascript rules because the feature uses Datalog queries.
    • Apply Promesa rules because the implementation composes async query/pull calls.
    • Apply DB model rules because the feature depends on page identity and :block/parent hierarchy semantics.

Testing plan

Implementation should follow @Test-Driven Development.

Add failing tests before implementation.

Unit tests in show_test.cljs

Add tests in:

/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/show_test.cljs

Recommended tests:

  1. build-action defaults :page-hierarchy? to false.

    • Call show-command/build-action without :page-hierarchy.
    • Assert [:action :page-hierarchy?] is false.
  2. build-action stores :page-hierarchy? when true.

    • Call show-command/build-action with {:page "Foo" :page-hierarchy true}.
    • Assert [:action :page-hierarchy?] is true.
  3. Default ordinary page behavior is unchanged.

    • Use a mock page Foo with content blocks under :block/page.
    • Also provide child pages under :block/parent.
    • Run execute-show without :page-hierarchy? or with false.
    • Assert content blocks appear.
    • Assert hierarchy-only child pages do not appear unless they are also content blocks.
    • Assert queries include the :block/page path for Foo.
  4. Ordinary page hierarchy renders when enabled.

    • Arrange Foo -> Bar -> Baz through :block/parent page entities.
    • Run execute-show with :page "Foo" and :page-hierarchy? true.
    • Assert output contains Foo, Bar, and Baz in tree order.
    • Assert page content blocks from Foo, Bar, or Baz are not inlined.
    • Assert queries use :block/parent for hierarchy children.
  5. Ordinary page hierarchy filters non-display children.

    • Under Foo, include one ordinary page, one class page, one property page, and one non-page block.
    • Run with :page-hierarchy? true.
    • Assert only the ordinary page appears.
  6. Ordinary page hierarchy respects --level.

    • Arrange Foo -> Bar -> Baz.
    • Run with :level 2 and :page-hierarchy? true.
    • Assert Bar appears and Baz does not.
  7. Structured output uses existing shape.

    • Run with output format :json or :edn.
    • Assert child pages are under [:data :root :block/children].
    • Assert :block/uuid is stripped.
    • Assert no :show/* internal keys leak.
  8. Library behavior remains unchanged.

    • Existing Library tests should still pass without setting :page-hierarchy?.
    • Add a small assertion that :page-hierarchy? false still renders Library hierarchy if needed.
  9. Page id and page UUID targets are consistent if supported.

    • Run show --id <page-id> with :page-hierarchy? true against a page entity.
    • Run show --uuid <page-uuid> with :page-hierarchy? true against a page entity.
    • Assert both render page hierarchy.
    • If implementation intentionally scopes the feature to --page, assert and document the narrower behavior instead.
  10. Block targets are not changed by the flag.

    • Run show --id <block-id> with :page-hierarchy? true.
    • Assert the existing block subtree behavior remains unchanged.
  11. Existing thread APIs only.

    • Record invoked method keywords for ordinary page hierarchy mode.
    • Assert every method is in the existing allowed set, such as :thread-api/pull, :thread-api/q, :thread-api/get-block-refs, and :thread-api/get-block-parents when applicable.
    • Assert no new method keyword is introduced.
  12. Page hierarchy cycle fails fast.

    • Arrange malformed page parents such as Foo -> Bar -> Baz -> Bar.
    • Run with :page-hierarchy? true.
    • Assert execution rejects with the chosen cycle error code.

Command/help tests

If existing command tests assert help output or option lists, update them to include --page-hierarchy.

Candidate file:

/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs

Add or update tests only where the command option list is already validated.

CLI e2e tests

Add CLI e2e coverage in:

/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_cases.edn

Recommended e2e shape:

  1. Create or import a graph with namespace pages such as Foo/Bar/Baz.
  2. Add ordinary content blocks to Foo or Bar so the test can distinguish content mode from hierarchy mode.
  3. Run:
logseq show --graph <graph> --page Foo --page-hierarchy true --linked-references false
  1. Assert stdout contains Foo, Bar, and Baz.
  2. Assert stdout does not contain ordinary page content blocks that should only appear in default content mode.
  3. Run:
logseq show --graph <graph> --page Foo --linked-references false
  1. Assert default stdout still contains normal page content and does not switch to hierarchy mode.
  2. Add a JSON assertion if stable fixture paths are available:
logseq show --graph <graph> --page Foo --page-hierarchy true --output json --linked-references false

Assert a stable child title path such as:

[:data :root :block/children 0 :block/title]

Update:

/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_inventory.edn

if the new e2e cases extend tracked command/option coverage.

Verification commands

Before implementation, run the newly added focused tests and confirm they fail for the missing feature.

After implementation, run:

bb dev:test -v logseq.cli.command.show-test

If command/help tests are changed, run:

bb dev:test -v logseq.cli.commands-test

If CLI e2e cases are added, rebuild the CLI when necessary and run the relevant e2e suite or case, for example:

bb -f cli-e2e/bb.edn test --skip-build

Before submitting, run:

bb dev:lint-and-test

when the local environment supports the full suite.

Review plan after implementation

Run logseq-review-workflow after the implementation and tests are complete.

The review scope should include every changed file, likely:

src/main/logseq/cli/command/show.cljs
src/test/logseq/cli/command/show_test.cljs
src/test/logseq/cli/commands_test.cljs if changed
cli-e2e/spec/non_sync_cases.edn if changed
cli-e2e/spec/non_sync_inventory.edn if changed
docs/cli/logseq-cli.md if changed

The review should load and apply:

rules/common.md
rules/modules/logseq-cli.md
rules/libraries/clojure-cljs.md
rules/libraries/datascript.md
rules/libraries/promesa.md
rules/modules/db-model.md

The review should specifically check:

  • The implementation did not add a new thread API without documented necessity.
  • --page-hierarchy defaults to false and does not change ordinary show --page <page> output by default.
  • --page-hierarchy true displays child page hierarchy through :block/parent, not normal page content blocks.
  • The existing show --page Library behavior remains available without requiring the new option.
  • The implementation reuses the Library hierarchy traversal where practical instead of duplicating divergent code paths.
  • Normal block show, normal page content show, --id, --uuid, multi-id, linked block, linked references, property rendering, UUID-ref footer, JSON, and EDN paths still behave correctly.
  • Page hierarchy child filtering is intentional and covered by tests.
  • Page hierarchy traversal cannot recurse forever on malformed parent cycles.
  • Datascript queries are scoped by parent id and do not scan more data than necessary.
  • Promise composition returns promises consistently and does not accidentally return unresolved nested promises.
  • Structured output does not leak internal traversal metadata.
  • Tests cover behavior and failure modes rather than only implementation details.

If the review finds issues, fix them and rerun the focused tests before finalizing the implementation.