diff --git a/docs/agent-guide/010-logseq-cli-show-linked-references.md b/docs/agent-guide/010-logseq-cli-show-linked-references.md index aed6232eef..f420b962c2 100644 --- a/docs/agent-guide/010-logseq-cli-show-linked-references.md +++ b/docs/agent-guide/010-logseq-cli-show-linked-references.md @@ -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 `[[]]` references with `[[]]`, 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 `[[]]` with `[[]]`, 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 `[[]]` references should render those tokens replaced with `[[]]`. 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 `[[]]` tokens with `[[]]`, 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. diff --git a/src/main/logseq/cli/command/show.cljs b/src/main/logseq/cli/command/show.cljs index d738bb3a0e..f37a7e45e9 100644 --- a/src/main/logseq/cli/command/show.cljs +++ b/src/main/logseq/cli/command/show.cljs @@ -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)}})))) diff --git a/src/test/logseq/cli/commands_test.cljs b/src/test/logseq/cli/commands_test.cljs index de8a888ee5..4bfa7635a6 100644 --- a/src/test/logseq/cli/commands_test.cljs +++ b/src/test/logseq/cli/commands_test.cljs @@ -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" diff --git a/src/test/logseq/cli/integration_test.cljs b/src/test/logseq/cli/integration_test.cljs index ef5941fe0d..884ba41607 100644 --- a/src/test/logseq/cli/integration_test.cljs +++ b/src/test/logseq/cli/integration_test.cljs @@ -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")]