Show source-note buttons: link answers back to SilverBullet pages #4

Closed
opened 2026-07-04 21:51:43 +08:00 by weiwen · 1 comment
Owner

After an answer, attach inline URL buttons that deep-link to the SilverBullet pages pi touched, so you can jump from an answer to the note it read or updated.

Key finding — not blocked

The note parked this as "blocked: pi doesn't emit source metadata." That's wrong: pi emits every file operation as a Content::ToolCall { name, arguments } in the agent_end messages. src/pi.rs:244 already logs the tool name + arguments and discards them. Capturing them gives an exact list of notes touched.

(Follow-up suggestions — "here are 3 next questions" — remain out of scope: pi doesn't emit them as structured data. Not part of this issue.)

Which tool calls carry a usable path (verified against pi 0.80.3)

  • read{ path, offset?, limit? }: the note pi consulted. Use.
  • edit / write{ path, ... }: the note pi modified (the "file into notes" case). Use — a link to "the note I just updated" is useful.
  • grep{ pattern, path?, glob? }: path is only a search scope, not the matched files (those are in the tool result, which evie doesn't capture). Ignore.
  • find / ls — directory scope, no source file. Ignore.

Sources = union of read + edit/write paths, deduped, first-seen order.

Scope

  1. In read_agent_response, capture path from read/edit/write ToolCall args on agent_end. Dedupe, preserve first-seen order. Return them alongside the final text (extend the return type / thread a second channel).
  2. Filter to paths inside notes_dir; normalise relative paths (pi runs with current_dir(notes_dir)).
  3. Map each path to a SilverBullet page name: strip .md, URL-encode path segments, keep / folder separators. Build <base_url>/<PageName>.
  4. Add config silverbullet.base_url (optional). If unset, skip buttons entirely — no error.
  5. Attach an InlineKeyboardMarkup of URL buttons (one per note, cap ~5, labelled by page name) to the final message only (on the final editMessageText, via .reply_markup(...)). URL buttons need no callback_query handler.

Constraints

  • Buttons go only on the last page when an answer spans multiple messages (MessageView).
  • Non-note tool calls (bash, grep, find, ls) and paths outside notes_dir are ignored.
  • Streaming path unchanged; buttons appear only once the answer is final.

Acceptance

  • Ask something that makes pi read notes -> final message carries URL buttons opening those pages in SilverBullet.
  • File-into-notes turn -> a button links to the note pi created/updated.
  • silverbullet.base_url unset -> no buttons, no error.
  • Turns that touch no notes -> no buttons.
After an answer, attach inline **URL buttons** that deep-link to the SilverBullet pages pi touched, so you can jump from an answer to the note it read or updated. ## Key finding — not blocked The note parked this as *"blocked: pi doesn't emit source metadata."* That's wrong: pi emits every file operation as a `Content::ToolCall { name, arguments }` in the `agent_end` messages. `src/pi.rs:244` already *logs* the tool name + arguments and discards them. Capturing them gives an exact list of notes touched. (Follow-up *suggestions* — "here are 3 next questions" — remain out of scope: pi doesn't emit them as structured data. Not part of this issue.) ## Which tool calls carry a usable path (verified against pi 0.80.3) - **`read`** — `{ path, offset?, limit? }`: the note pi consulted. **Use.** - **`edit` / `write`** — `{ path, ... }`: the note pi modified (the "file into notes" case). **Use** — a link to "the note I just updated" is useful. - **`grep`** — `{ pattern, path?, glob? }`: `path` is only a search *scope*, not the matched files (those are in the tool *result*, which evie doesn't capture). **Ignore.** - **`find` / `ls`** — directory scope, no source file. **Ignore.** Sources = union of `read` + `edit`/`write` paths, deduped, first-seen order. ## Scope 1. In `read_agent_response`, capture `path` from `read`/`edit`/`write` `ToolCall` args on `agent_end`. Dedupe, preserve first-seen order. Return them alongside the final text (extend the return type / thread a second channel). 2. Filter to paths inside `notes_dir`; normalise relative paths (pi runs with `current_dir(notes_dir)`). 3. Map each path to a SilverBullet page name: strip `.md`, URL-encode path segments, keep `/` folder separators. Build `<base_url>/<PageName>`. 4. Add config `silverbullet.base_url` (optional). If unset, skip buttons entirely — no error. 5. Attach an `InlineKeyboardMarkup` of **URL buttons** (one per note, cap ~5, labelled by page name) to the **final** message only (on the final `editMessageText`, via `.reply_markup(...)`). URL buttons need **no** `callback_query` handler. ## Constraints - Buttons go only on the last page when an answer spans multiple messages (`MessageView`). - Non-note tool calls (bash, grep, find, ls) and paths outside `notes_dir` are ignored. - Streaming path unchanged; buttons appear only once the answer is final. ## Acceptance - Ask something that makes pi read notes -> final message carries URL buttons opening those pages in SilverBullet. - File-into-notes turn -> a button links to the note pi created/updated. - `silverbullet.base_url` unset -> no buttons, no error. - Turns that touch no notes -> no buttons.
Author
Owner

Implemented in commit 158a24b.

What was done:

  • Added AgentResponse { text, sources } to pi.rs. read_agent_response now captures path from read/edit/write ToolCall arguments on agent_end events, deduped preserving first-seen order.
  • Added silverbullet.base_url (optional [silverbullet] config section). When set, the Telegram handler attaches an InlineKeyboardMarkup of URL buttons (max 5) to the final message pointing to each touched note.
  • Source paths are relative to notes_dir (pi runs with current_dir(notes_dir)). page_name_from_path strips .md, URL-encodes each segment, keeps / folder separators.
  • MessageView::apply_with_buttons attaches the keyboard only on the last page.
  • Non-note tool calls (grep, bash, find, ls) are ignored. Empty silverbullet.base_url = no buttons, no error.

Files changed: Cargo.toml, Cargo.lock, src/pi.rs, src/session.rs, src/http.rs, src/telegram/mod.rs, src/config.rs, src/main.rs

Tests: 30 unit tests pass (6 new for source capture), 2 integration tests pass.

Not closing — will be closed by admin.

Implemented in commit 158a24b. **What was done:** - Added `AgentResponse { text, sources }` to `pi.rs`. `read_agent_response` now captures `path` from `read`/`edit`/`write` ToolCall arguments on `agent_end` events, deduped preserving first-seen order. - Added `silverbullet.base_url` (optional `[silverbullet]` config section). When set, the Telegram handler attaches an `InlineKeyboardMarkup` of URL buttons (max 5) to the final message pointing to each touched note. - Source paths are relative to `notes_dir` (pi runs with `current_dir(notes_dir)`). `page_name_from_path` strips `.md`, URL-encodes each segment, keeps `/` folder separators. - `MessageView::apply_with_buttons` attaches the keyboard only on the last page. - Non-note tool calls (grep, bash, find, ls) are ignored. Empty `silverbullet.base_url` = no buttons, no error. **Files changed:** Cargo.toml, Cargo.lock, src/pi.rs, src/session.rs, src/http.rs, src/telegram/mod.rs, src/config.rs, src/main.rs **Tests:** 30 unit tests pass (6 new for source capture), 2 integration tests pass. Not closing — will be closed by admin.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
weiwen/evie#4
No description provided.