impl 010-logseq-cli-show-linked-references.md (1)

This commit is contained in:
rcmerci
2026-01-20 21:55:55 +08:00
parent 5223618110
commit 6b23c5926b
4 changed files with 351 additions and 15 deletions

View File

@@ -1,9 +1,9 @@
# Logseq CLI Show Linked References Implementation Plan
Goal: Add task status prefixes to show output and include linked references for the shown block or page.
Goal: Add task status prefixes and block tag rendering to show output, replace inline `[[<uuid>]]` references with `[[<referenced block content>]]`, and include linked references for the shown block or page.
Architecture: The CLI show command will fetch marker data with the tree and will build a display label that prefixes the marker before the block content.
Architecture: The CLI show command will also call db-worker-node thread-api/get-block-refs to fetch linked references and append them to text output while returning structured data in JSON and EDN output.
Architecture: The CLI show command will fetch marker data with the tree and will build a display label that prefixes the marker before the block content, replaces inline `[[<uuid>]]` with `[[<referenced block content>]]`, and then appends inline block tags (e.g. `#RTC #Task`) to the content display.
Architecture: The CLI show command will also call db-worker-node thread-api/get-block-refs to fetch linked references and append them to text output in a tree form (db/id in the first column), while returning structured data in JSON and EDN output.
Architecture: Data flow will remain CLI -> db-worker-node -> db-core with no new worker endpoints, reusing existing thread-api functions.
Tech Stack: ClojureScript, promesa, logseq-cli transport, db-worker-node thread-api, Datascript.
@@ -14,6 +14,7 @@ Related: Builds on docs/agent-guide/009-cli-add-pos-show-tree-align.md.
I will follow @test-driven-development and write failing tests before any production changes.
I will add a unit test in /Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs that asserts tree->text prefixes :block/marker before the block title for TODO and CANCELED blocks.
I will add a unit test in /Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs that asserts tree->text appends :block/tags in `#Tag` format to the rendered block content.
I will add a unit test in /Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs that asserts tree->text keeps multiline alignment when the marker prefix is present.
I will add an integration test in /Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs that creates a page and a referencing block, runs show with --format json, and asserts that linked references are present and include the referencing block uuid and page title.
I will run the new unit tests with bb dev:test -v logseq.cli.commands-test and the new integration test namespace with bb dev:test -v logseq.cli.integration-test to confirm failures, then again to confirm passing.
@@ -36,12 +37,12 @@ We need to enhance the show output to include task status prefixes and linked re
6. Add a failing integration test in /Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs that creates a page, adds a block referencing that page, runs show for the page in JSON, and asserts the response contains linked references with block uuid and page title.
7. Run the two new unit tests and the integration test to confirm failures for the expected reasons.
8. Update the tree-block selector in /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs to pull :block/marker alongside :block/title and :block/name.
9. Add a small helper in /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs that builds the display label by prefixing :block/marker followed by a space when a marker is present, while falling back to :block/name or :block/uuid when :block/title is missing.
9. Add a small helper in /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs that builds the display label by prefixing :block/marker followed by a space when a marker is present, while falling back to :block/name or :block/uuid when :block/title is missing, and appending `#Tag` strings for :block/tags.
10. Update tree->text in /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs to use the new label helper for both root and child nodes so that marker prefixes appear in all output lines.
11. Add a linked references fetch step in /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs that calls transport/invoke with :thread-api/get-block-refs using the root db/id.
12. Normalize linked references by pulling a minimal selector for each ref block, including :db/id, :block/uuid, :block/title, :block/marker, and {:block/page [:db/id :block/name :block/title :block/uuid]}, so CLI output is predictable and lightweight.
13. Extend the show tree data structure to include :linked-references with a list of normalized blocks and a :count, and ensure this structure is returned for JSON and EDN output paths.
14. For text output, append a Linked References section after the tree that lists each referencing block with its page title and marker-prefixed label, and show a count line when references exist.
14. For text output, append a Linked References section after the tree that renders each referencing block in tree form with db/id in the first column (aligned to the glyph column), include the marker-prefixed label, and show a count line when references exist.
15. Update the unit test expectations in /Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs to match the marker-prefixed text output.
16. Update the new integration test assertions to match the final JSON structure for linked references.
17. Run the targeted tests again, then bb dev:lint-and-test, to verify all changes pass.
@@ -49,9 +50,12 @@ We need to enhance the show output to include task status prefixes and linked re
## Edge cases
Blocks without :block/marker should render exactly as before with no extra spacing.
Blocks with :block/tags should render tag names as `#Tag` appended after the content.
Blocks containing inline `[[<uuid>]]` references should render those tokens replaced with `[[<referenced block content>]]`.
Blocks with :block/title nil should still render using :block/name or :block/uuid with the marker prefix applied only when a title exists.
Linked references can be empty, in which case the Linked References section should be omitted from text output and :linked-references should contain a zero count in JSON and EDN.
Linked reference blocks that are missing a page or title should still render using their uuid fallback.
Linked references should render using the same tree layout rules as the main show tree, including db/id in the first column.
Show by block id and show by page name should both resolve linked references using the root db/id.
## Testing Details
@@ -62,8 +66,8 @@ The integration test uses db-worker-node to create actual referencing blocks and
## Implementation Details
- Pull :block/marker in the tree selector so task status is available for label rendering.
- Build a label helper that prefixes markers without changing existing fallback logic for titles and names.
- Append a Linked References section in text output with a header, count, and marker-prefixed block labels.
- Build a label helper that prefixes markers without changing existing fallback logic for titles and names, replaces inline `[[<uuid>]]` tokens with `[[<referenced block content>]]`, and appends block tags (e.g. `#RTC #Task`) after the content.
- Append a Linked References section in text output with a header, count, and tree-formatted block labels (db/id in the first column).
- Use :thread-api/get-block-refs for reference discovery and re-pull a minimal selector for stable CLI output.
- Return linked references in JSON and EDN outputs as {:linked-references {:count n :blocks [...]}}.
- Keep all changes inside the CLI show command and avoid new db-worker-node endpoints.

View File

@@ -37,7 +37,195 @@
nil)))
(def ^:private tree-block-selector
[:db/id :block/uuid :block/title :block/order {:block/parent [:db/id]}])
[:db/id
:block/uuid
:block/title
:block/marker
:block/order
{:block/parent [:db/id]}
{:block/tags [:db/id :block/name :block/title :block/uuid]}])
(def ^:private linked-ref-selector
[:db/id
:block/uuid
:block/title
:block/marker
{:block/tags [:db/id :block/name :block/title :block/uuid]}
{:block/page [:db/id :block/name :block/title :block/uuid]}
{:block/parent [:db/id
:block/name
:block/title
:block/uuid
{:block/page [:db/id :block/name :block/title :block/uuid]}]}])
(declare tree->text)
(def ^:private uuid-ref-pattern #"\[\[([0-9a-fA-F-]{36})\]\]")
(defn- tag-label
[tag]
(or (:block/title tag)
(:block/name tag)
(some-> (:block/uuid tag) str)))
(defn- replace-uuid-refs
[value uuid->label]
(if (and (string? value) (seq uuid->label))
(string/replace value uuid-ref-pattern
(fn [[_ id]]
(if-let [label (get uuid->label (string/lower-case id))]
(str "[[" label "]]")
(str "[[" id "]]"))))
value))
(defn- tags->suffix
[tags]
(let [labels (->> tags
(map tag-label)
(remove string/blank?))]
(when (seq labels)
(string/join " " (map #(str "#" %) labels)))))
(defn- block-label
[node]
(let [title (:block/title node)
marker (:block/marker node)
uuid->label (:uuid->label node)
base (cond
(and title (seq marker)) (str marker " " title)
title title
(:block/name node) (:block/name node)
(:block/uuid node) (some-> (:block/uuid node) str))
base (replace-uuid-refs base uuid->label)
tags-suffix (tags->suffix (:block/tags node))]
(cond
(and base tags-suffix) (str base " " tags-suffix)
tags-suffix tags-suffix
:else base)))
(defn- page-label
[block]
(let [page (:block/page block)]
(or (:block/title page)
(:block/name page)
(some-> (:block/uuid page) str)
(some-> (:block/uuid block) str))))
(defn- fetch-linked-references
[config repo root-id]
(p/let [refs (transport/invoke config :thread-api/get-block-refs false [repo root-id])
ref-ids (vec (keep :db/id refs))
pulled (if (seq ref-ids)
(p/all (map (fn [id]
(transport/invoke config :thread-api/pull false [repo linked-ref-selector id]))
ref-ids))
[])]
(let [blocks (vec (remove nil? pulled))
page-id-from (fn [block]
(let [page (:block/page block)
parent (:block/parent block)
parent-page (:block/page parent)]
(or (when (map? page) (:db/id page))
(when (number? page) page)
(when (map? parent-page) (:db/id parent-page))
(when (number? parent-page) parent-page)
(when (map? parent) (:db/id parent)))))
page-ids (->> blocks
(keep page-id-from)
distinct
vec)]
(p/let [pages (if (seq page-ids)
(p/all (map (fn [id]
(transport/invoke config :thread-api/pull false
[repo [:db/id :block/name :block/title :block/uuid] id]))
page-ids))
[])]
(let [page-id->page (zipmap page-ids pages)
blocks (mapv (fn [block]
(let [page (:block/page block)
parent (:block/parent block)
parent-page (:block/page parent)
page-id (page-id-from block)
page (or (when (map? page)
(select-keys page [:db/id :block/name :block/title :block/uuid]))
(when (map? parent-page)
(select-keys parent-page [:db/id :block/name :block/title :block/uuid]))
(get page-id->page page-id)
(when (map? parent)
(select-keys parent [:db/id :block/name :block/title :block/uuid]))
(when (or (:block/title block) (:block/name block) (:block/uuid block))
(select-keys block [:db/id :block/name :block/title :block/uuid])))]
(cond-> (dissoc block :block/parent)
page (assoc :block/page page))))
blocks)]
{:count (count blocks)
:blocks blocks})))))
(defn- linked-refs->text
[blocks uuid->label]
(let [page-key (fn [block]
(let [page (:block/page block)]
(or (:db/id page)
(:block/uuid page)
(page-label block))))
page-node (fn [block]
(let [page (:block/page block)]
(cond
(map? page) page
(some? page) {:db/id page}
:else {})))
groups (->> blocks
(group-by page-key)
(sort-by (fn [[_ page-blocks]]
(page-label (first page-blocks)))))]
(string/join
"\n\n"
(map (fn [[_ page-blocks]]
(let [root (page-node (first page-blocks))
root (assoc root :block/children (vec page-blocks))]
(tree->text {:root root :uuid->label uuid->label})))
groups))))
(defn- extract-uuid-refs
[value]
(->> (re-seq uuid-ref-pattern (or value ""))
(map second)
(filter common-util/uuid-string?)
distinct))
(defn- collect-uuid-refs
[{:keys [root]} linked-refs]
(let [collect-nodes (fn collect-nodes [node]
(if-let [children (:block/children node)]
(into [node] (mapcat collect-nodes children))
[node]))
nodes (when root (collect-nodes root))
ref-blocks (:blocks linked-refs)
pages (keep :block/page ref-blocks)
texts (->> (concat nodes ref-blocks pages)
(mapcat (fn [node] (keep node [:block/title :block/name])))
(remove string/blank?))]
(->> texts
(mapcat extract-uuid-refs)
distinct
vec)))
(defn- fetch-uuid-labels
[config repo uuid-strings]
(if (seq uuid-strings)
(p/let [blocks (p/all (map (fn [uuid-str]
(transport/invoke config :thread-api/pull false
[repo [:block/uuid :block/title :block/name]
[:block/uuid (uuid uuid-str)]]))
uuid-strings))]
(->> blocks
(remove nil?)
(map (fn [block]
(let [uuid-str (some-> (:block/uuid block) str)]
[(string/lower-case uuid-str)
(or (:block/title block) (:block/name block) uuid-str)])))
(into {})))
(p/resolved {})))
(defn- fetch-blocks-for-page
[config repo page-id]
@@ -69,7 +257,9 @@
(cond
(some? id)
(p/let [entity (transport/invoke config :thread-api/pull false
[repo [:db/id :block/name :block/uuid :block/title {:block/page [:db/id :block/title]}] id])]
[repo [:db/id :block/name :block/uuid :block/title :block/marker
{:block/page [:db/id :block/title]}
{:block/tags [:db/id :block/name :block/title :block/uuid]}] id])]
(if-let [page-id (get-in entity [:block/page :db/id])]
(p/let [blocks (fetch-blocks-for-page config repo page-id)
children (build-tree blocks (:db/id entity) max-depth)]
@@ -84,12 +274,16 @@
(if-not (common-util/uuid-string? uuid-str)
(p/rejected (ex-info "block must be a uuid" {:code :invalid-block}))
(p/let [entity (transport/invoke config :thread-api/pull false
[repo [:db/id :block/name :block/uuid :block/title {:block/page [:db/id :block/title]}]
[repo [:db/id :block/name :block/uuid :block/title :block/marker
{:block/page [:db/id :block/title]}
{:block/tags [:db/id :block/name :block/title :block/uuid]}]
[:block/uuid (uuid uuid-str)]])
entity (if (:db/id entity)
entity
(transport/invoke config :thread-api/pull false
[repo [:db/id :block/name :block/uuid :block/title {:block/page [:db/id :block/title]}]
[repo [:db/id :block/name :block/uuid :block/title :block/marker
{:block/page [:db/id :block/title]}
{:block/tags [:db/id :block/name :block/title :block/uuid]}]
[:block/uuid uuid-str]]))]
(if-let [page-id (get-in entity [:block/page :db/id])]
(p/let [blocks (fetch-blocks-for-page config repo page-id)
@@ -103,7 +297,9 @@
(seq page-name)
(p/let [page-entity (transport/invoke config :thread-api/pull false
[repo [:db/id :block/uuid :block/title] [:block/name page-name]])]
[repo [:db/id :block/uuid :block/title :block/marker
{:block/tags [:db/id :block/name :block/title :block/uuid]}]
[:block/name page-name]])]
(if-let [page-id (:db/id page-entity)]
(p/let [blocks (fetch-blocks-for-page config repo page-id)
children (build-tree blocks page-id max-depth)]
@@ -114,9 +310,9 @@
(p/rejected (ex-info "block or page required" {:code :missing-target})))))
(defn tree->text
[{:keys [root]}]
[{:keys [root uuid->label]}]
(let [label (fn [node]
(or (:block/title node) (:block/name node) (str (:block/uuid node))))
(or (block-label (assoc node :uuid->label uuid->label)) "-"))
node-id (fn [node]
(or (:db/id node) "-"))
collect-nodes (fn collect-nodes [node]
@@ -157,6 +353,18 @@
(walk root "")
(string/join "\n" @lines)))
(defn- tree->text-with-linked-refs
[{:keys [linked-references uuid->label] :as tree-data}]
(let [tree-text (tree->text tree-data)
refs (:blocks linked-references)
count (:count linked-references)]
(if (seq refs)
(str tree-text
"\n\n"
"Linked References (" count ")\n"
(linked-refs->text refs uuid->label))
tree-text)))
(defn build-action
[options repo]
(if-not (seq repo)
@@ -182,6 +390,15 @@
[action config]
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
tree-data (fetch-tree cfg action)
root-id (get-in tree-data [:root :db/id])
linked-refs (if root-id
(fetch-linked-references cfg (:repo action) root-id)
{:count 0 :blocks []})
uuid-refs (collect-uuid-refs tree-data linked-refs)
uuid->label (fetch-uuid-labels cfg (:repo action) uuid-refs)
tree-data (assoc tree-data
:linked-references linked-refs
:uuid->label uuid->label)
format (:format action)]
(case format
"edn"
@@ -195,4 +412,4 @@
:output-format :json}
{:status :ok
:data {:message (tree->text tree-data)}}))))
:data {:message (tree->text-with-linked-refs tree-data)}}))))

View File

@@ -204,6 +204,80 @@
"175 └── cccc")
(tree->text tree-data))))))
(deftest test-tree->text-prefixes-marker
(testing "show tree text prefixes markers before block titles"
(let [tree->text #'show-command/tree->text
tree-data {:root {:db/id 1
:block/title "Root"
:block/marker "TODO"
:block/children [{:db/id 2
:block/title "Child"
:block/marker "CANCELED"}]}}]
(is (= (str "1 TODO Root\n"
"2 └── CANCELED Child")
(tree->text tree-data))))))
(deftest test-tree->text-marker-multiline-alignment
(testing "show tree text keeps multiline alignment when marker prefix is present"
(let [tree->text #'show-command/tree->text
tree-data {:root {:db/id 1
:block/title "Root"
:block/children [{:db/id 22
:block/title "line1\nline2"
:block/marker "TODO"}]}}]
(is (= (str "1 Root\n"
"22 └── TODO line1\n"
" line2")
(tree->text tree-data))))))
(deftest test-tree->text-linked-references-tree
(testing "show tree text renders linked references as trees with db/id in first column"
(let [tree->text-with-linked-refs #'show-command/tree->text-with-linked-refs
tree-data {:root {:db/id 1
:block/title "Root"}
:linked-references {:count 2
:blocks [{:db/id 10
:block/title "Ref A"
:block/marker "TODO"
:block/page {:db/id 100
:block/title "Page A"}}
{:db/id 11
:block/title "Ref B"
:block/page {:db/id 101
:block/title "Page B"}}]}}]
(is (= (str "1 Root\n"
"\n"
"Linked References (2)\n"
"100 Page A\n"
"10 └── TODO Ref A\n"
"\n"
"101 Page B\n"
"11 └── Ref B")
(tree->text-with-linked-refs tree-data))))))
(deftest test-tree->text-appends-tags
(testing "show tree text appends block tags to content"
(let [tree->text #'show-command/tree->text
tree-data {:root {:db/id 1
:block/title "Root"
:block/children [{:db/id 2
:block/title "Child"
:block/tags [{:block/title "RTC"}
{:block/name "task"}]}]}}]
(is (= (str "1 Root\n"
"2 └── Child #RTC #task")
(tree->text tree-data))))))
(deftest test-tree->text-replaces-uuid-refs
(testing "show tree text replaces inline [[uuid]] with referenced block content"
(let [tree->text #'show-command/tree->text
uuid "11111111-1111-1111-1111-111111111111"
tree-data {:root {:db/id 1
:block/title (str "See [[" uuid "]]")}
:uuid->label {(string/lower-case uuid) "Target block"}}]
(is (= (str "1 See [[Target block]]")
(tree->text tree-data))))))
(deftest test-list-subcommand-parse
(testing "list page parses"
(let [result (commands/parse-args ["list" "page"

View File

@@ -286,6 +286,47 @@
(is false (str "unexpected error: " e))
(done)))))))
(deftest test-cli-show-linked-references
(async done
(let [data-dir (node-helper/create-tmp-dir "db-worker-linked-refs")]
(-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
_ (fs/writeFileSync cfg-path "{:output-format :json}")
_ (run-cli ["graph" "create" "--repo" "linked-refs-graph"] data-dir cfg-path)
_ (run-cli ["--repo" "linked-refs-graph" "add" "page" "--page" "TargetPage"] data-dir cfg-path)
_ (run-cli ["--repo" "linked-refs-graph" "add" "page" "--page" "SourcePage"] data-dir cfg-path)
list-page-result (run-cli ["--repo" "linked-refs-graph" "list" "page" "--expand"]
data-dir cfg-path)
list-page-payload (parse-json-output list-page-result)
page-item (some (fn [item]
(when (= "TargetPage" (or (:block/title item) (:title item)))
item))
(get-in list-page-payload [:data :items]))
page-id (or (:db/id page-item) (:id page-item))
blocks-edn (str "[{:block/title \"Ref to TargetPage\" :block/refs [{:db/id " page-id "}]}]")
_ (run-cli ["--repo" "linked-refs-graph" "add" "block" "--target-page-name" "SourcePage"
"--blocks" blocks-edn] data-dir cfg-path)
show-result (run-cli ["--repo" "linked-refs-graph" "show" "--page-name" "TargetPage" "--format" "json"]
data-dir cfg-path)
show-payload (parse-json-output show-result)
linked (get-in show-payload [:data :linked-references])
ref-block (first (:blocks linked))
stop-result (run-cli ["server" "stop" "--repo" "linked-refs-graph"] data-dir cfg-path)
stop-payload (parse-json-output stop-result)]
(is (= "ok" (:status show-payload)))
(is (some? page-id))
(is (map? linked))
(is (pos? (:count linked)))
(is (seq (:blocks linked)))
(is (some? ref-block))
(is (some? (or (:block/uuid ref-block) (:uuid ref-block))))
(is (some? (or (get-in ref-block [:page :title])
(get-in ref-block [:page :name]))))
(is (= "ok" (:status stop-payload)))
(done))
(p/catch (fn [e]
(is false (str "unexpected error: " e))
(done)))))))
(deftest test-cli-graph-export-import-edn
(async done
(let [data-dir (node-helper/create-tmp-dir "db-worker-export-edn")]