Merge branch 'master' into enhance/pdf-annotation-asset-blocks

This commit is contained in:
charlie
2026-05-28 10:00:07 +08:00
24 changed files with 370 additions and 158 deletions

View File

@@ -29,7 +29,7 @@ Use `logseq` to inspect and edit graph entities, run Datascript queries, and con
- `doctor`
- `sync status|start|stop|upload|download|remote-graphs|ensure-keys|grant-access|config set|get|unset`
- Authentication: `login|logout`
- Utilities: `agent bridge|agent bridge list`, `completion`, `debug`, `example`, `skill`
- Utilities: `agent bridge`, `completion`, `debug`, `example`, `skill`
## Global options
@@ -118,7 +118,6 @@ Use `logseq` to inspect and edit graph entities, run Datascript queries, and con
- `query list` returns both built-ins and `custom-queries` from `cli.edn`.
- `agent bridge --dry-run` checks the local bridge setup, resolves `:agent-name` or hostname, registers the AgentBridge name in the graph, finds routable `#Task` TODO blocks assigned to that AgentBridge name, and prints the Codex commands it would run without starting Codex or writing `agent-session-id`.
- `agent bridge` starts/reuses db-worker-node, listens to db-worker-node events, scans routable tasks on startup and each event, starts `codex exec --json` for matched tasks, stores the Codex session/thread id in `agent-bridge-sessions.edn`, and writes it to the task's `agent-session-id` property.
- `agent bridge list` reads root-dir scoped bridge session records and hides completed sessions by default; use `--all` to include them.
- `show --id` accepts either one db/id or an EDN vector of ids.
- `remove block --id` also accepts one db/id or an EDN vector.
- `upsert block` enters update mode when `--id` or `--uuid` is provided.

View File

@@ -26,7 +26,7 @@ root_dir=""
graph="agent-bridge-demo-$(date +%s)"
timeout_sec=45
agent_name="AgentBridgeDemo"
task_title="AgentBridge demo task: mark this block done"
task_title="AgentBridge demo task: mark this block in review"
expected_session="thread-agent-bridge-demo"
bridge_pid=""
graph_created=0
@@ -247,7 +247,7 @@ agent_session=""
while (( SECONDS < deadline )); do
task_status="$(query_task_status "$task_id")"
agent_session="$(query_agent_session "$task_id")"
if [[ "$task_status" == "logseq.property/status.done" && "$agent_session" == "$expected_session" ]]; then
if [[ "$task_status" == "logseq.property/status.in-review" && "$agent_session" == "$expected_session" ]]; then
break
fi
if [[ -n "${bridge_pid:-}" ]] && ! kill -0 "$bridge_pid" 2>/dev/null; then
@@ -259,8 +259,8 @@ while (( SECONDS < deadline )); do
sleep 0.5
done
if [[ "$task_status" != "logseq.property/status.done" ]]; then
echo "Expected task status done, got: ${task_status:-<empty>}" >&2
if [[ "$task_status" != "logseq.property/status.in-review" ]]; then
echo "Expected task status in-review, got: ${task_status:-<empty>}" >&2
cat "$bridge_log" >&2
cat "$bridge_err" >&2
exit 1
@@ -293,6 +293,6 @@ if "Block UUID:" not in prompt:
raise SystemExit("block uuid missing from Codex prompt")
PY
echo "task status: done"
echo "task status: in-review"
echo "agent-session-id: $agent_session"
echo "agent bridge demo completed"

View File

@@ -8,7 +8,7 @@ import subprocess
import time
TASK_TITLE = "测试 agent bridge 功能把当前task status设置为done"
TASK_TITLE = "Test AgentBridge task routing and completion status"
EXPECTED_SESSION = "thread-e2e-agent-bridge"
PARALLEL_TASK_TITLES = [
"测试 agent bridge 并行执行任务 1",

View File

@@ -1278,23 +1278,22 @@
:extends :non-sync/graph-json-env}
{:id "agent-bridge-workflows",
:setup
["mkdir -p '{{tmp-dir}}/assigned-root' && printf '{:output-format :json}\\n' > '{{tmp-dir}}/assigned-root/cli.edn' && {{cli}} --root-dir '{{tmp-dir}}/assigned-root' --config '{{tmp-dir}}/assigned-root/cli.edn' --output json graph create --graph cli-e2e-agent-bridge-assigned >/dev/null && {{cli}} --root-dir '{{tmp-dir}}/assigned-root' --config '{{tmp-dir}}/assigned-root/cli.edn' --output json upsert task --graph cli-e2e-agent-bridge-assigned --target-page AgentBridgeE2E --content '测试 agent bridge 功能把当前task status设置为done' --status todo >/dev/null"
["mkdir -p '{{tmp-dir}}/assigned-root' && printf '{:output-format :json}\\n' > '{{tmp-dir}}/assigned-root/cli.edn' && {{cli}} --root-dir '{{tmp-dir}}/assigned-root' --config '{{tmp-dir}}/assigned-root/cli.edn' --output json graph create --graph cli-e2e-agent-bridge-assigned >/dev/null && {{cli}} --root-dir '{{tmp-dir}}/assigned-root' --config '{{tmp-dir}}/assigned-root/cli.edn' --output json upsert task --graph cli-e2e-agent-bridge-assigned --target-page AgentBridgeE2E --content 'Test AgentBridge task routing and completion status' --status todo >/dev/null"
"mkdir -p '{{tmp-dir}}/parallel-root' && printf '{:output-format :json}\\n' > '{{tmp-dir}}/parallel-root/cli.edn' && {{cli}} --root-dir '{{tmp-dir}}/parallel-root' --config '{{tmp-dir}}/parallel-root/cli.edn' --output json graph create --graph cli-e2e-agent-bridge-parallel >/dev/null"
"mkdir -p '{{tmp-dir}}/comment-root' && printf '{:output-format :json}\\n' > '{{tmp-dir}}/comment-root/cli.edn' && {{cli}} --root-dir '{{tmp-dir}}/comment-root' --config '{{tmp-dir}}/comment-root/cli.edn' --output json graph create --graph cli-e2e-agent-bridge-comment >/dev/null"],
:cmds
["python3 '{{repo-root}}/cli-e2e/scripts/agent_bridge_e2e.py' --cli '{{repo-root}}/static/logseq-cli.js' --root-dir '{{tmp-dir}}/assigned-root' --config '{{tmp-dir}}/assigned-root/cli.edn' --graph cli-e2e-agent-bridge-assigned --tmp-dir '{{tmp-dir}}/assigned-work' --repo-root '{{repo-root}}' --assign-after-start"
"python3 '{{repo-root}}/cli-e2e/scripts/agent_bridge_e2e.py' --cli '{{repo-root}}/static/logseq-cli.js' --root-dir '{{tmp-dir}}/parallel-root' --config '{{tmp-dir}}/parallel-root/cli.edn' --graph cli-e2e-agent-bridge-parallel --tmp-dir '{{tmp-dir}}/parallel-work' --repo-root '{{repo-root}}' --parallel-assignment-check"
"python3 '{{repo-root}}/cli-e2e/scripts/agent_bridge_e2e.py' --cli '{{repo-root}}/static/logseq-cli.js' --root-dir '{{tmp-dir}}/comment-root' --config '{{tmp-dir}}/comment-root/cli.edn' --graph cli-e2e-agent-bridge-comment --tmp-dir '{{tmp-dir}}/comment-work' --repo-root '{{repo-root}}' --comment-mention-check"
"bash '{{repo-root}}/cli-e2e/scripts/agent_bridge_demo.sh' --cli '{{repo-root}}/static/logseq-cli.js' --root-dir '{{tmp-dir}}/demo-root' --graph cli-e2e-agent-bridge-demo --repo-root '{{repo-root}}'"
"mkdir -p '{{tmp-dir}}/list-root' && printf '{:output-format :json}\\n' > '{{tmp-dir}}/list-root/cli.edn' && {{cli}} --root-dir '{{tmp-dir}}/list-root' --config '{{tmp-dir}}/list-root/cli.edn' --output json agent bridge list --all"],
"bash '{{repo-root}}/cli-e2e/scripts/agent_bridge_demo.sh' --cli '{{repo-root}}/static/logseq-cli.js' --root-dir '{{tmp-dir}}/demo-root' --graph cli-e2e-agent-bridge-demo --repo-root '{{repo-root}}'"],
:expect
{:exit 0,
:stdout-json-paths {[:status] "ok", [:data :sessions] []}},
:stdout-contains ["agent bridge"]},
:covers
{:commands ["agent bridge" "agent bridge list"],
{:commands ["agent bridge"],
:options
{:global ["--config" "--graph" "--root-dir" "--output"],
:agent ["--all"]}},
:agent ["--dry-run"]}},
:cleanup
["{{cli}} --root-dir '{{tmp-dir}}/assigned-root' --config '{{tmp-dir}}/assigned-root/cli.edn' --output json server stop --graph cli-e2e-agent-bridge-assigned"
"{{cli}} --root-dir '{{tmp-dir}}/parallel-root' --config '{{tmp-dir}}/parallel-root/cli.edn' --output json server stop --graph cli-e2e-agent-bridge-parallel"

View File

@@ -155,9 +155,8 @@
:options ["--dev-script"]}
:agent
{:commands ["agent bridge"
"agent bridge list"]
:options ["--all"]}
{:commands ["agent bridge"]
:options ["--dry-run"]}
:completion
{:commands ["completion"]

View File

@@ -39,6 +39,29 @@
(or (block-title pvalue)
(:logseq.property/value pvalue)))
(defn- referenced-property-value-contents
[db property]
(if (= :db.type/ref (:db/valueType property))
(->> (d/datoms db :avet (:db/ident property))
(keep (fn [datom]
(some->> (:v datom)
(d/entity db)
property-value-content)))
set)
#{}))
(defn- closed-values-for-export
[db property]
(let [referenced-contents (referenced-property-value-contents db property)]
(->> (concat (entity-plus/lookup-kv-then-entity property :property/closed-values)
(filter #(contains? referenced-contents (property-value-content %))
(:block/_closed-value-property property)))
(reduce (fn [closed-values value]
(assoc closed-values (:db/id value) value))
{})
vals
(sort-by :block/order))))
(defn- shallow-copy-page
"Given a page or journal entity, shallow copies it e.g. no properties or tags info included.
Pages that are shallow copied are at the edges of export and help keep the export size reasonable and
@@ -147,7 +170,7 @@
(->> user-property-idents
(map (fn [ident]
(let [property (d/entity db ident)
closed-values (entity-plus/lookup-kv-then-entity property :property/closed-values)]
closed-values (closed-values-for-export db property)]
[property
(cond-> (select-keys property
(-> (disj db-property/schema-properties :logseq.property/classes)

View File

@@ -553,6 +553,58 @@
(get-in export-edn [:classes :user.class/MyClass :build/class-properties])))
(is (not (contains? (:properties export-edn) legacy-property)))))
(deftest graph-export-omits-legacy-plugin-property-schema-attrs
(let [plugin-property :plugin.property.degrande-colors/mugpet_degrande_colors_controls
conn (db-test/create-conn-with-import-map
{:properties {plugin-property {:logseq.property/type :json}}
:pages-and-blocks [{:page {:block/title "page1"}
:blocks [{:block/title "b1"}]}]})
plugin-property-ent (d/entity @conn plugin-property)
_ (d/transact! conn [{:db/id (:db/id plugin-property-ent)
:hide? true
:public? false}])
export-edn (sqlite-export/build-export @conn {:export-type :graph})
validation (sqlite-export/validate-export export-edn)]
(is (nil? (:error validation)))
(is (= {:logseq.property/type :json
:db/cardinality :db.cardinality/one
:block/title "mugpet_degrande_colors_controls"}
(get-in export-edn [:properties plugin-property])))))
(deftest graph-export-keeps-referenced-recycled-closed-value-config
(let [property-id :plugin.property.degrande-colors/tldraw
closed-value-uuid (random-uuid)
conn (db-test/create-conn-with-import-map
{:properties {property-id {:logseq.property/type :default
:build/closed-values [{:value "tldraw"
:uuid closed-value-uuid}]}}
:pages-and-blocks [{:page {:block/title "page1"}
:blocks [{:block/title "b1"
:build/properties {property-id [:block/uuid closed-value-uuid]}}]}]})
closed-value (d/entity @conn [:block/uuid closed-value-uuid])
_ (d/transact! conn [{:db/id (:db/id closed-value)
:logseq.property/deleted-at 1}])
export-edn (sqlite-export/build-export @conn {:export-type :graph})
validation (sqlite-export/validate-export export-edn)]
(is (nil? (:error validation)))
(is (= [{:value "tldraw" :uuid closed-value-uuid}]
(get-in export-edn [:properties property-id :build/closed-values])))))
(deftest graph-export-ignores-scalar-values-when-finding-referenced-closed-values
(let [property-id :user.property/datetime
conn (db-test/create-conn-with-import-map
{:properties {property-id {:logseq.property/type :datetime}}
:pages-and-blocks [{:page {:block/title "page1"}
:blocks [{:block/title "b1"
:build/properties {property-id 1779841453610}}]}]})
export-edn (sqlite-export/build-export @conn {:export-type :graph})
validation (sqlite-export/validate-export export-edn)]
(is (nil? (:error validation)))
(is (= {:logseq.property/type :datetime
:db/cardinality :db.cardinality/one
:block/title "datetime"}
(get-in export-edn [:properties property-id])))))
(deftest import-view-blocks
(let [original-data
;; Test a mix of page and block types

View File

@@ -113,7 +113,6 @@ The top-level help groups commands into graph inspection/editing, graph manageme
- `login`
- `logout`
- `agent bridge`
- `agent bridge list`
- `completion`
- `debug pull`
- `skill show`
@@ -126,8 +125,6 @@ The top-level help groups commands into graph inspection/editing, graph manageme
`agent bridge` is the first AgentBridge command surface. It resolves the target graph using normal CLI config precedence, resolves the AgentBridge name from `:agent-name` in `cli.edn` or the machine hostname, checks that `codex` is available, starts or reuses the graph's db-worker-node, registers the AgentBridge name in the graph, scans TODO `#Task` blocks assigned to that AgentBridge name, and listens to db-worker-node events for follow-up scans. Matched tasks are routed to `codex exec --json`; the bridge records the Codex session/thread id in `agent-bridge-sessions.edn` and writes it to the task's `agent-session-id` property. `--dry-run` performs the setup and scan but only prints the Codex commands it would run.
`agent bridge list` reads root-dir scoped bridge session records from `agent-bridge-sessions.edn`. Human output uses the columns `SESSION`, `STATUS`, `BACKEND`, `GRAPH`, `BLOCK`, `AGENT`, `STARTED`, and `UPDATED`, ending with `Count: N`. Completed sessions are hidden by default; use `--all` to include them.
## Global flags and output modes
Global flags are defined in `logseq.cli.command.core/global-spec`:

View File

@@ -2605,6 +2605,8 @@
util/caret-range)
mobile-range (when mobile? (get-cursor-range))]
(when (and (not forbidden-edit?) (contains? #{1 0} button))
(when (= 1 button)
(block-selection/set-pointer-down!))
(cond
(and meta? shift?)
(when-not (empty? selection-blocks)
@@ -3680,6 +3682,7 @@
(defn- select-block-under-pointer!
[selection-block-ids scroll-direction]
(when (and (seq selection-block-ids)
(block-selection/pointer-down?)
(or (state/get-selection-start-block)
(seq (state/get-selection-blocks))))
(when-let [block-dom-node (or (visible-selection-boundary-block selection-block-ids scroll-direction)

View File

@@ -1,5 +1,21 @@
(ns frontend.components.block.selection)
(defonce *pointer-is-down? (atom false))
(defn set-pointer-down!
[]
(reset! *pointer-is-down? true))
(defn clear-pointer-down!
([]
(reset! *pointer-is-down? false))
([_]
(clear-pointer-down!)))
(defn pointer-down?
[]
(true? @*pointer-is-down?))
(defn select-on-hover?
[{:keys [last-client-y client-y dragging? editing-same-block? active-selection?]}]
(and (or (not= last-client-y client-y)

View File

@@ -2,6 +2,7 @@
(:require [cljs-drag-n-drop.core :as dnd]
[clojure.string :as string]
[dommy.core :as d]
[frontend.components.block.selection :as block-selection]
[frontend.components.content :as cp-content]
[frontend.components.find-in-page :as find-in-page]
[frontend.components.handbooks :as handbooks]
@@ -382,6 +383,9 @@
(mixins/event-mixin
(fn [state]
(mixins/listen state js/window "pointerdown" hide-context-menu-and-clear-selection)
(mixins/listen state js/window "pointerup" block-selection/clear-pointer-down!)
(mixins/listen state js/window "pointercancel" block-selection/clear-pointer-down!)
(mixins/listen state js/window "blur" block-selection/clear-pointer-down!)
(mixins/listen state js/window "keydown"
(fn [e]
(cond

View File

@@ -1668,7 +1668,7 @@
[:div
[:div (t :plugin/checking-for-updates)]
(when sub-content [:p.opacity-60 sub-content])]
(ui/loading ""))
:info)
(when uid (notification/clear! uid))))
[check-pending? sub-content])

View File

@@ -11,6 +11,7 @@
[frontend.util :as util]
[logseq.common.config :as common-config]
[logseq.common.graph-registry :as graph-registry]
[logseq.common.uuid :as common-uuid]
[logseq.db :as ldb]
[promesa.core :as p]))
@@ -96,6 +97,20 @@
(ldb/get-graph-local-uuid db*))
str))))
(defn- new-local-graph-uuid
[]
(uuid (str "00000000" (subs (str (common-uuid/gen-uuid)) 8))))
(defn- <ensure-local-graph-uuid!
[repo db*]
(if-let [local-graph-uuid (ldb/get-graph-local-uuid db*)]
(p/resolved local-graph-uuid)
(let [local-graph-uuid (new-local-graph-uuid)]
(p/let [_ (db/transact! repo
[(ldb/kv :logseq.kv/local-graph-uuid local-graph-uuid)]
{:graph-open/ensure-local-graph-uuid? true})]
local-graph-uuid))))
(defn remember-current-graph-id-in-tab!
[]
(when-let [repo (state/get-current-repo)]
@@ -106,13 +121,15 @@
[]
(when-let [repo (state/get-current-repo)]
(when-let [db* (db/get-db repo)]
(<upsert-graph-registry-entry!
{:repo repo
:graph-name (common-config/strip-leading-db-version-prefix repo)
:local-graph-id (some-> (ldb/get-graph-local-uuid db*) str)
:graph-id (some-> (or (ldb/get-graph-rtc-uuid db*)
(ldb/get-graph-local-uuid db*))
str)}))))
(p/let [local-graph-uuid (<ensure-local-graph-uuid! repo db*)
db* (or (db/get-db repo) db*)
graph-uuid (or (ldb/get-graph-rtc-uuid db*)
local-graph-uuid)]
(<upsert-graph-registry-entry!
{:repo repo
:graph-name (common-config/strip-leading-db-version-prefix repo)
:local-graph-id (str local-graph-uuid)
:graph-id (some-> graph-uuid str)})))))
(defn settle-metadata-to-local!
[m]

View File

@@ -354,6 +354,7 @@
[(t :plugin/up-to-date ":)") :success]
[error-code :error])
rate-limit-error? (some-> msg str (string/includes? "API rate limit"))
pending? (seq (:plugin/updates-pending @state/state))]
(if (and only-check pending?)
@@ -365,11 +366,12 @@
(state/consume-updates-from-coming-plugin! payload true))
;; notify human tips
(notification/show!
(str
(if (= :error type) "[Error]" "")
"<" (:id payload) "> "
msg) type)))
(when-not rate-limit-error?
(notification/show!
(str
(if (= :error type) "[Error]" "")
"<" (:id payload) "> "
msg) type))))
(when-not fake-error?
(js/console.error "Update Error:" (:error-code payload))))

View File

@@ -108,8 +108,6 @@ DROP TRIGGER IF EXISTS blocks_au;
(defn- throw-upsert-blocks-error!
[item]
(js/console.error "Upsert blocks wrong data: ")
(js/console.dir item)
(throw (ex-info "Search upsert-blocks wrong data: "
(bean/->clj item))))
@@ -537,6 +535,10 @@ DROP TRIGGER IF EXISTS blocks_au;
(string/join " "))
title)))
(defn- block-result-title
[block]
(db-content/recur-replace-uuid-in-block-title block))
(defn- matched-alias
[q block]
(when-not (string/blank? q)
@@ -669,13 +671,13 @@ DROP TRIGGER IF EXISTS blocks_au;
(when (include-search-block? conn block code-class option)
(let [alias-source (some-> (first (:block/_alias block))
(select-keys [:block/uuid :block/title]))
alias-match (matched-alias q block)
display-title (if (:enable-snippet? option)
(if (page-or-object? block)
(ensure-highlighted-snippet snippet (:block/title block) q)
(ensure-highlighted-snippet snippet title q))
(if (page-or-object? block)
(:block/title block)
alias-match (matched-alias q block)
page-or-object-result? (page-or-object? block)
result-title (if page-or-object-result? (block-result-title block) title)
display-title (if (:enable-snippet? option)
(ensure-highlighted-snippet snippet result-title q)
(if page-or-object-result?
result-title
(or snippet title)))
block-page (or
(:block/uuid (:block/page block))

View File

@@ -20,23 +20,13 @@
{:dry-run {:desc "Print Codex commands without starting Codex or writing agent-session-id"
:coerce :boolean}})
(def ^:private bridge-list-spec
{:all {:desc "Include completed sessions"
:coerce :boolean}})
(def entries
[(core/command-entry ["agent" "bridge"]
:agent-bridge
"Run task agent bridge"
bridge-spec
{:examples ["logseq agent bridge --graph my-graph"
"logseq agent bridge --graph my-graph --dry-run"]})
(core/command-entry ["agent" "bridge" "list"]
:agent-bridge-list
"List agent bridge sessions"
bridge-list-spec
{:examples ["logseq agent bridge list"
"logseq agent bridge list --all"]})])
"logseq agent bridge --graph my-graph --dry-run"]})])
(defn- trim-non-empty
[value]
@@ -75,11 +65,6 @@
:graph graph
:dry-run? (boolean (:dry-run options))}})
:agent-bridge-list
{:ok? true
:action {:type :agent-bridge-list
:all? (boolean (:all options))}}
{:ok? false
:error {:code :unknown-command
:message (str "unknown agent command: " command)}}))
@@ -456,19 +441,6 @@
:status status
:updated-at (js/Date.now)}))
(defn list-sessions
[config {:keys [all?]}]
(let [sessions (vec (:sessions (read-session-store config)))]
(if all?
sessions
(vec (remove #(= :completed (:status %)) sessions)))))
(defn execute-list
[action config]
{:status :ok
:command :agent-bridge-list
:data {:sessions (list-sessions config {:all? (:all? action)})}})
(defn- now-iso
[]
(.toISOString (js/Date.)))
@@ -873,6 +845,17 @@
{}]))]
true)))
(defn- mark-agent-bridge-task-in-review!
[cfg repo block]
(let [block-uuid (:block/uuid block)]
(p/let [_ (transport/invoke cfg :thread-api/apply-outliner-ops
[repo [[:batch-set-property [[block-uuid]
:logseq.property/status
:logseq.property/status.in-review
{}]]]
{}])]
true)))
(def ^:private routable-task-query
'[:find [(pull ?e [:db/id
:block/uuid
@@ -953,11 +936,18 @@
(emit-log! cfg (log-line (str "Codex command prepared for " (block-uuid-str block) ": " preview)))
(p/let [{:keys [session]} (start-codex! command
{:on-exit (fn [code session-id]
(when session-id
(update-session-status! cfg session-id
(if (zero? (or code 1))
:completed
:failed))))})
(let [success? (zero? (or code 1))]
(when session-id
(update-session-status! cfg session-id
(if success?
:completed
:failed))
(when success?
(-> (p/let [cfg* (cli-server/ensure-server! cfg repo)
_ (mark-agent-bridge-task-in-review! cfg* repo block)]
true)
(p/catch (fn [e]
(log/error :agent-bridge-task-in-review-failed e))))))))})
_ (when-not (seq session)
(throw (ex-info "codex session id missing"
{:code :codex-session-id-missing})))

View File

@@ -1320,23 +1320,21 @@
:logseq.property.asset/checksum (:asset/checksum metadata)
:block/tags #{asset-tag-id}})
blocks)))
block-uuid (get-in action* [:blocks 0 :block/uuid])
_ (when-not (uuid? block-uuid)
(throw (ex-info "created asset block missing uuid"
{:code :asset-create-failed})))
_ (copy-asset-file-to-graph! config
(:repo action)
block-uuid
(:asset/type metadata)
asset-path)
create-result (add-command/execute-add-block (assoc action* :type :add-block) config)
created-ids (vec (or (get-in create-result [:data :result]) []))
created-id (first created-ids)
_ (when-not (some? created-id)
(throw (ex-info "asset block not created"
{:code :asset-create-failed})))
created-entity (pull-entity-by-id cfg (:repo action) [:db/id :block/uuid] created-id)
block-uuid (:block/uuid created-entity)
_ (when-not (uuid? block-uuid)
(throw (ex-info "created asset block missing uuid"
{:code :asset-create-failed
:id created-id})))
_ (copy-asset-file-to-graph! config
(:repo action)
block-uuid
(:asset/type metadata)
asset-path)]
{:code :asset-create-failed})))]
{:status :ok
:data {:result [created-id]}})

View File

@@ -325,6 +325,14 @@
:else
nil))
(defn- validate-agent-bridge
[summary {:keys [command args cmds]}]
(when (and (= command :agent-bridge)
(seq args))
(command-core/unknown-command-result
summary
(str "unknown command: " (string/join " " (concat cmds args))))))
(defn- validate-graph-sync-and-completion
[summary {:keys [command opts import-export-type completion-shell-error]}]
(cond
@@ -379,6 +387,7 @@
(or (validate-write-and-upsert summary validation-context)
(validate-target-query-and-search summary validation-context)
(validate-option-contracts summary validation-context)
(validate-agent-bridge summary validation-context)
(validate-graph-sync-and-completion summary validation-context)))
(defn- command-has-content?
@@ -589,7 +598,7 @@
(:graph-list :graph-create :graph-switch :graph-remove :graph-validate :graph-info)
(graph-command/build-graph-action command graph repo options)
(:agent-bridge :agent-bridge-list)
:agent-bridge
(agent-command/build-action command options repo graph)
:graph-backup-list
@@ -701,7 +710,6 @@
(case (:type action)
:graph-list (graph-command/execute-graph-list action config)
:agent-bridge (agent-command/execute-bridge action config)
:agent-bridge-list (agent-command/execute-list action config)
:graph-backup-list (graph-command/execute-graph-backup-list action config)
:graph-backup-create (graph-command/execute-graph-backup-create action config)
:graph-backup-restore (graph-command/execute-graph-backup-restore action config)

View File

@@ -576,26 +576,6 @@
(str table "\n\n" warning)
table)))
(defn- format-agent-bridge-list
[sessions now-ms]
(let [session-field (fn [value]
(cond
(keyword? value) (name value)
(nil? value) nil
:else (str value)))]
(format-counted-table
["SESSION" "STATUS" "BACKEND" "GRAPH" "BLOCK" "AGENT" "STARTED" "UPDATED"]
(mapv (fn [session]
[(session-field (:session session))
(session-field (:status session))
(session-field (:backend session))
(session-field (:graph session))
(session-field (:block session))
(session-field (:agent session))
(human-ago (:started-at session) now-ms)
(human-ago (:updated-at session) now-ms)])
(or sessions [])))))
(defn- format-agent-bridge
[data]
(if (seq (:logs data))
@@ -1051,7 +1031,6 @@
:server-list (format-server-list (:servers data)
(get-in human [:server-list :revision-mismatch]))
:agent-bridge (format-agent-bridge data)
:agent-bridge-list (format-agent-bridge-list (:sessions data) now-ms)
:server-cleanup (format-server-cleanup data)
(:server-start :server-stop :server-restart)
(format-server-action command data)

View File

@@ -2,6 +2,17 @@
(:require [cljs.test :refer [are deftest is]]
[frontend.components.block.selection :as selection]))
(deftest pointer-down-state-lifecycle
(selection/clear-pointer-down!)
(is (false? (selection/pointer-down?)))
(selection/set-pointer-down!)
(is (true? (selection/pointer-down?)))
(selection/clear-pointer-down!)
(is (false? (selection/pointer-down?)))
(selection/set-pointer-down!)
(selection/clear-pointer-down! #js {:type "pointerup"})
(is (false? (selection/pointer-down?))))
(deftest select-on-hover-keeps-active-selection-while-scroll-moves-block-under-pointer
(is (true?
(selection/select-on-hover?

View File

@@ -1,10 +1,14 @@
(ns frontend.handler.graph-test
(:require [cljs.test :refer [async deftest is testing]]
[datascript.core :as d]
[frontend.db :as db]
[frontend.common.idb :as idb]
[frontend.handler.graph]
[frontend.state :as state]
[frontend.util.url :as url-util]
[logseq.common.graph-registry :as graph-registry]
[logseq.db :as ldb]
[logseq.db.frontend.schema :as db-schema]
[promesa.core :as p]))
(deftest graph-registry-key-is-indexeddb-compatible-test
@@ -49,6 +53,42 @@
:graph-id "remote-uuid"}
@stored-graph))))))
(deftest upsert-current-graph-registry-repairs-missing-local-graph-uuid-test
(async done
(let [upsert-current-f (some-> (resolve 'frontend.handler.graph/<upsert-current-graph-registry!) deref)
conn (d/create-conn db-schema/schema)
registry-entry (atom nil)]
(is (fn? upsert-current-f) "Current graph registry upsert should exist")
(d/transact! conn [{:db/ident :logseq.kv/schema-version
:kv/value db-schema/version}])
(p/with-redefs [state/get-current-repo (constantly "logseq_db_broken")
db/get-db (fn
([repo]
(when (= "logseq_db_broken" repo) @conn))
([repo deref?]
(when (= "logseq_db_broken" repo)
(if deref? @conn conn))))
db/transact! (fn [repo tx-data tx-meta]
(is (= "logseq_db_broken" repo))
(d/transact! conn tx-data tx-meta)
(p/resolved nil))
frontend.handler.graph/<upsert-graph-registry-entry!
(fn [entry]
(reset! registry-entry entry)
(p/resolved nil))]
(-> (upsert-current-f)
(.then (fn [_]
(let [local-graph-uuid (ldb/get-graph-local-uuid @conn)]
(is (uuid? local-graph-uuid))
(is (= (str local-graph-uuid)
(:local-graph-id @registry-entry)))
(is (= (:local-graph-id @registry-entry)
(:graph-id @registry-entry)))
(done))))
(.catch (fn [e]
(is false (str e))
(done))))))))
(deftest resolve-startup-repo-prefers-tab-repo-before-global-current-test
(let [resolve-f (some-> (resolve 'frontend.handler.graph/resolve-startup-repo) deref)]
(is (fn? resolve-f) "Startup repo resolver should exist")

View File

@@ -618,6 +618,36 @@
(is (= "Artificial Intelligence" (:block/title result)))
(is (not (contains? result :alias))))))))
(deftest search-result-replaces-page-title-uuid-refs-without-alias-title
(testing "page result titles resolve uuid refs but do not display alias titles from the search index"
(let [page-id #uuid "00000000-0000-0000-0000-000000000246"
ref-id #uuid "00000000-0000-0000-0000-000000000247"
page {:db/id 1
:block/uuid page-id
:block/title (str "Artificial [[" ref-id "]]")
:block/refs [{:block/uuid ref-id
:block/title "Machine Learning"}]
:block/alias [{:db/id 2
:block/uuid #uuid "00000000-0000-0000-0000-000000000248"
:block/title "ai"}]}]
(with-redefs [d/entity (fn [_db [_attr id]]
(when (= id page-id)
page))
ldb/page? (fn [entity] (= (:db/id entity) (:db/id page)))
ldb/object? (constantly false)
ldb/built-in? (constantly false)
ldb/hidden? (constantly false)]
(let [result (#'search/search-result->block-result
(atom :db)
"Artificial"
nil
{:enable-snippet? false}
{:id (str page-id)
:page (str page-id)
:title "Artificial [[Machine Learning]] ai"})]
(is (= "Artificial [[Machine Learning]]" (:block/title result)))
(is (not (contains? result :alias))))))))
(deftest search-result-snippet-uses-canonical-page-title
(testing "page snippets do not display alias titles from the search index"
(let [page-id #uuid "00000000-0000-0000-0000-000000000242"

View File

@@ -130,14 +130,16 @@
(deftest test-agent-command-entries
(testing "parse agent bridge command surface"
(let [bridge (commands/parse-args ["agent" "bridge" "--graph" "demo" "--dry-run"])
list-result (commands/parse-args ["agent" "bridge" "list"])]
(let [bridge (commands/parse-args ["agent" "bridge" "--graph" "demo" "--dry-run"])]
(is (true? (:ok? bridge)))
(is (= :agent-bridge (:command bridge)))
(is (= "demo" (get-in bridge [:options :graph])))
(is (true? (get-in bridge [:options :dry-run])))
(is (true? (:ok? list-result)))
(is (= :agent-bridge-list (:command list-result)))))
(is (true? (get-in bridge [:options :dry-run])))))
(testing "agent bridge list is not a command"
(let [result (commands/parse-args ["agent" "bridge" "list"])]
(is (false? (:ok? result)))
(is (= :unknown-command (get-in result [:error :code])))))
(testing "top-level help exposes the agent utility group"
(let [summary (:summary (commands/parse-args ["--help"]))]
@@ -166,14 +168,14 @@
(is (= "logseq_db_demo" (get-in result [:action :repo])))
(is (true? (get-in result [:action :dry-run?])))))
(testing "agent bridge list is root-dir scoped and does not require graph"
(testing "agent bridge list action is not supported"
(let [result (commands/build-action {:ok? true
:command :agent-bridge-list
:options {}
:args []}
{})]
(is (true? (:ok? result)))
(is (= :agent-bridge-list (get-in result [:action :type]))))))
(is (false? (:ok? result)))
(is (= :unknown-command (get-in result [:error :code]))))))
(deftest test-resolve-agent-name
(testing "config overrides hostname"
@@ -1264,6 +1266,72 @@
(is false (str "unexpected error: " e))))
(p/finally done)))))
(deftest test-agent-bridge-task-route-marks-successful-completion-in-review
(async done
(let [block (task-block {})
route-opts {:repo "logseq_db_demo"
:graph "demo"
:agent-name "build-host"
:prompt-templates {:task "Task {{graph}} {{block-uuid}} {{agent-name}}\n{{task-block-tree}}"}}
route! (fn []
(#'agent-command/route-task! {:root-dir "/tmp/logseq"}
route-opts
{:block block
:tree-text "- Ship the CLI bridge"}))
run-scenario
(fn [exit-code]
(let [ops* (atom [])
session-statuses* (atom [])]
(p/let [_ (p/with-redefs [transport/invoke
(fn [_ method args]
(case method
:thread-api/q
(if (string/includes? (pr-str args) ":logseq.property/status")
(p/resolved :logseq.property/status.todo)
(p/resolved nil))
:thread-api/apply-outliner-ops
(let [[_ ops _] args]
(swap! ops* into ops)
(p/resolved {:ok true}))
(p/rejected (ex-info "unexpected invoke"
{:method method
:args args}))))
cli-server/ensure-server! (fn [cfg _repo]
(assoc cfg :base-url "http://127.0.0.1:1234"))
agent-command/start-codex! (fn [_command opts]
((:on-exit opts) exit-code "session-123")
(p/resolved {:session "session-123"
:status :running}))
agent-command/record-session! (fn [_cfg _session-record]
true)
agent-command/update-session-status! (fn [_cfg session-id status]
(swap! session-statuses* conj [session-id status])
true)
agent-command/write-agent-session-id! (fn [_cfg _repo _block-uuid _session-id]
(p/resolved true))]
(p/let [_ (route!)
_ (p/delay 10)]
true))]
{:ops @ops*
:session-statuses @session-statuses*})))
in-review-op [:batch-set-property [[(:block/uuid block)]
:logseq.property/status
:logseq.property/status.in-review
{}]]]
(-> (p/let [success (run-scenario 0)
failure (run-scenario 1)]
(is (= [["session-123" :completed]]
(:session-statuses success)))
(is (some #(= in-review-op %) (:ops success)))
(is (= [["session-123" :failed]]
(:session-statuses failure)))
(is (not-any? #(= in-review-op %) (:ops failure))))
(p/catch (fn [e]
(is false (str "unexpected error: " e))))
(p/finally done)))))
(deftest test-agent-bridge-task-route-does-not-mark-started-before-session-id-is-written
(async done
(let [block (task-block {})
@@ -1390,15 +1458,10 @@
:agent "build-host"
:started-at 1000
:updated-at 3000})
(testing "list hides completed sessions by default"
(is (= ["codex-running"]
(mapv :session (agent-command/list-sessions config {})))))
(testing "list can include completed sessions"
(is (= ["codex-running" "codex-done"]
(mapv :session (agent-command/list-sessions config {:all? true})))))
(testing "session file is EDN data"
(let [payload (reader/read-string (fs/readFileSync (agent-command/session-store-path config) "utf8"))]
(is (= 2 (count (:sessions payload))))))
(is (= ["codex-running" "codex-done"]
(mapv :session (:sessions payload))))))
(finally
(fs/rmSync root #js {:recursive true :force true})))))
@@ -1415,7 +1478,8 @@
:agent "build-host"
:started-at 1000
:updated-at 2000})
(let [session (first (agent-command/list-sessions config {:all? true}))]
(let [payload (reader/read-string (fs/readFileSync (agent-command/session-store-path config) "utf8"))
session (first (:sessions payload))]
(is (= :completed (:status session)))
(is (= :codex (:backend session)))
(is (= "demo" (:graph session)))
@@ -1426,32 +1490,6 @@
(finally
(fs/rmSync root #js {:recursive true :force true})))))
(deftest test-execute-agent-bridge-list-and-format
(let [root (temp-root)]
(try
(agent-command/record-session! {:root-dir root}
{:session "codex-running"
:status :running
:backend :codex
:graph "demo"
:block "11111111-1111-1111-1111-111111111111"
:agent "build-host"
:started-at 1000
:updated-at 2000})
(let [result (agent-command/execute-list {:type :agent-bridge-list} {:root-dir root})
output (cli-format/format-result result {:output-format :human :now-ms 3000})]
(is (= :ok (:status result)))
(is (string/includes? output "SESSION"))
(is (string/includes? output "STATUS"))
(is (string/includes? output "BACKEND"))
(is (string/includes? output "codex-running"))
(is (string/includes? output "running"))
(is (not (string/includes? output ":running")))
(is (not (string/includes? output ":codex")))
(is (string/includes? output "Count: 1")))
(finally
(fs/rmSync root #js {:recursive true :force true})))))
(deftest test-format-agent-bridge-logs
(let [output (cli-format/format-result {:status :ok
:command :agent-bridge
@@ -1575,7 +1613,8 @@
:logseq.property/status.doing
{}]]]]]
@calls))
(let [sessions (agent-command/list-sessions {:root-dir root} {})]
(let [payload (reader/read-string (fs/readFileSync (agent-command/session-store-path {:root-dir root}) "utf8"))
sessions (:sessions payload)]
(is (= [{:session "session-123"
:status :running
:backend :codex

View File

@@ -77,6 +77,7 @@
(deftest test-execute-upsert-asset-create-applies-metadata-and-copies-file
(async done
(let [add-actions* (atom [])
lifecycle-calls* (atom [])
copy-calls* (atom [])
action {:type :upsert-asset
:mode :create
@@ -84,16 +85,19 @@
:graph "demo-graph"
:asset-path "/tmp/logo.png"
:content "Logo"
:blocks [{:block/title "Logo"}] }]
:blocks [{:block/title "Logo"
:block/uuid (uuid "00000000-0000-0000-0000-000000000101")}] }]
(-> (p/with-redefs [cli-server/ensure-server! (fn [config _repo]
(p/resolved (assoc config :base-url "http://example")))
upsert-command/asset-file-exists? (fn [_] true)
upsert-command/asset-file-size-bytes (fn [_] 123)
upsert-command/asset-file-checksum (fn [_] "sha-256-value")
upsert-command/copy-asset-file-to-graph! (fn [_ repo block-uuid asset-type source-path]
(swap! lifecycle-calls* conj :copy)
(swap! copy-calls* conj [repo block-uuid asset-type source-path])
"/tmp/copied/logo.png")
add-command/execute-add-block (fn [add-action _]
(swap! lifecycle-calls* conj :add-block)
(swap! add-actions* conj add-action)
(p/resolved {:status :ok
:data {:result [101]}}))
@@ -127,7 +131,8 @@
(uuid "00000000-0000-0000-0000-000000000101")
"png"
"/tmp/logo.png"]]
@copy-calls*))))
@copy-calls*))
(is (= [:copy :add-block] @lifecycle-calls*))))
(p/catch (fn [e]
(is false (str "unexpected error: " e))))
(p/finally done)))))
@@ -773,4 +778,3 @@
(p/catch (fn [e]
(is false (str "unexpected error: " e))))
(p/finally done)))))