Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ captures a clean SVG without changing the active search. `/keys`, theme
switching, filtered-row colors, focus repair, and repeated-key quit
confirmation now behave consistently across the two layouts.

#### Durable bookmarks across CLI and TUI (#79)

agentgrep can now save content, record, or thread handles without copying
prompt bodies or changing the underlying histories. `agentgrep bookmark`
offers idempotent add, remove, list, and JSON output for terminal workflows,
while the HUD adds a focus-safe `b` shortcut and `/bookmark` command.

The `/bookmarks` recall list resolves saved handles against the current stores,
keeps unavailable choices visible, and reopens available records without
replacing the active search results. See {ref}`the bookmark guide
<cli-bookmark>` for scope and privacy details.

## agentgrep 0.1.0a50 (2026-08-09)

agentgrep 0.1.0a50 makes the store catalogue describe an agent's whole
Expand Down
115 changes: 115 additions & 0 deletions docs/cli/bookmark.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
(cli-bookmark)=

# agentgrep bookmark

`agentgrep bookmark` saves a small, durable pointer to something you found
without copying its prompt or conversation into another database. The pointer
is a complete canonical ID from {ref}`ADR 0015
<adr-deterministic-record-identity>`:

| Scope | Target | Meaning |
| --- | --- | --- |
| `content` | `agc1:` | Equal normalized role, kind, and text |
| `record` | `agr1:`; `bookmark add` also needs `--content-id` | One logical stored occurrence, checked against its `agc1:` content |
| `thread` | `agt1:` | One backend thread with a defensible native anchor |

There are no short IDs or prefix lookups. Copy the complete canonical ID from a
search result. `--content-id` is required for `bookmark add` when its target is
an `agr1:` record ID; the complete content ID prevents a stale or mismatched
record bookmark from opening different content. `bookmark remove` does not
accept that option; removal needs only the complete target ID.

## Add, list, and remove

Save a content bookmark:

```console
$ agentgrep bookmark add agc1:2vlm1978v1np5kg5fkqv539kic
```

Save an exact record bookmark with its content validator:

```console
$ agentgrep bookmark add \
--content-id agc1:2vlm1978v1np5kg5fkqv539kic \
agr1:uuqn9q331f1fcgsr5gr8agefhs
```

Save a thread bookmark:

```console
$ agentgrep bookmark add agt1:bkd9k19ok4vvbsf73jornija04
```

List saved bookmarks in creation order:

```console
$ agentgrep bookmark list
```

Emit the same list as deterministic JSON:

```console
$ agentgrep bookmark list --json
```

Remove a bookmark by its complete target ID:

```console
$ agentgrep bookmark remove agc1:2vlm1978v1np5kg5fkqv539kic
```

Add and remove are idempotent. Human output reports `added`, `removed`, or
`unchanged`; `--json` exposes the same action for scripts. Re-adding an existing
target and removing an absent target both succeed as `unchanged`.

For a record target, re-add returns `unchanged` only when `--content-id` matches
the same saved content validator. A different valid `agc1:` validator is a
validation failure and exits `1`; it is not treated as unchanged.

The default capacity is 200 bookmarks. At capacity, a matching re-add still
returns `unchanged`, removal still works, and a new target is refused without
changing the saved list. Successful operations exit `0`; storage, validation,
capacity, and corruption failures exit `1`. Argument errors are reported by the
parser before the command runs.

(cli-bookmark-storage)=

## Private local state

Bookmarks live in agentgrep-owned state under the XDG data directory. When
`XDG_DATA_HOME` is set, the snapshot is
`$XDG_DATA_HOME/agentgrep/bookmarks.json`; otherwise agentgrep uses the XDG
default. The snapshot stores canonical IDs, scope, creation time, and the
content validator required for a record bookmark. It does not store prompt
text, titles, source paths, working directories, or repository paths.

Canonical IDs are pseudonymous equality handles, not secrets or anonymization.
The saved IDs and creation times can still reveal activity, so agentgrep keeps
the application directory and files private. If the snapshot is malformed,
uses an unknown schema, has duplicate targets, or exceeds capacity, agentgrep
reports a path-free corruption error and refuses it as a whole rather than
salvaging or overwriting it.

(cli-bookmark-resolution)=

## Recall in the TUI

The CLI manages canonical targets; it does not scan history to resolve them.
Use the HUD's {ref}`bookmark recall <tui-bookmarks>` to compare saved targets
with the current stores and open an available record. A missing target remains
saved, so it can resolve again if its store becomes available later.

Bookmark writes affect only agentgrep-owned state. Discovery and recall keep
Codex, Claude Code, Cursor, and every other source store read-only.

## Command

```{eval-rst}
.. argparse::
:module: agentgrep
:func: build_docs_parser
:prog: agentgrep
:path: bookmark
:nodescription:
```
7 changes: 7 additions & 0 deletions docs/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ Ranked, deduped search grouped by session — best matches first.
Enumerate on-disk stores with fd-shaped flag grammar.
:::

:::{grid-item-card} agentgrep bookmark
:link: bookmark
:link-type: doc
Save private canonical pointers without copying prompt content.
:::

:::{grid-item-card} agentgrep ui
:link: tui
:link-type: ref
Expand Down Expand Up @@ -172,5 +178,6 @@ $ agentgrep
grep
search
find
bookmark
reference
```
106 changes: 106 additions & 0 deletions docs/dev/adr/0016-durable-bookmarks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
(adr-durable-bookmarks)=

# ADR 0016: Durable bookmarks

## Status

Accepted.

## Context

Search is deliberately read-only over agent history stores, but users still
need a durable way to return to a useful result. Copying prompt bodies, source
paths, or backend metadata into a second index would enlarge the privacy and
migration surface. Saving a physical `agref1:` locator would instead tie the
bookmark to one store location and adapter revision.

{ref}`ADR 0015 <adr-deterministic-record-identity>` provides the smaller
contract this feature needs: canonical content, logical occurrence, and thread
handles with explicit availability limits. Bookmarks persist those handles as
local user intent and resolve them against current records only when requested.

## Decision

### Canonical scopes

Every bookmark has exactly one scope and one complete canonical target:

| Scope | Target | Recall semantics |
| --- | --- | --- |
| `record` | `agr1:` plus its `agc1:` content validator | Require both IDs to match one current logical occurrence. |
| `thread` | `agt1:` | Open a representative current record from the matching thread. |
| `content` | `agc1:` | Open an equal-content occurrence currently available. |

No scope accepts a shortened handle, canonical-ID prefix lookup, physical
`agref1:` locator, or path. A record bookmark persists `content_id` as a second
check because resolving an occurrence to different content is worse than
leaving it unresolved. Thread and content bookmarks do not pretend to select a
particular occurrence.

Resolution runs a fresh, un-deduplicated search over all supported agents and
both prompt and conversation scopes. It hashes candidates away from the TUI
message pump and stops once all targets have either matched or the current scan
has finished. When no match is available, unresolved bookmarks remain saved; a
source may be unavailable now and return later. Opening one record changes only
the detail pane and does not replace the user's loaded result list.

### Bounded, idempotent mutations

Add and remove are idempotent. Adding an existing target returns `unchanged`
when its validator agrees; removing an absent target also returns `unchanged`.
A new target returns `added`, an existing target returns `removed` when toggled,
and a successful explicit removal returns `removed`.

The default capacity is 200. Capacity is checked only for a new target, so
re-adding or removing remains possible when the store is full. A new target is
refused without evicting an older choice. Creation order is preserved; there
is no implicit least-recently-used policy.

### Storage and privacy

The versioned JSON snapshot lives in agentgrep's XDG data directory, separate
from query history and from every discovered source. Its top level contains
`schema_version` and an ordered entry list. Each entry contains only
`target_id`, `scope`, `content_id`, and `created_at`; `content_id` is null
outside record scope.

The snapshot excludes prompt text, titles, source paths, working directories,
repository paths, agent metadata, and physical refs. Canonical IDs are
pseudonymous comparison handles rather than secrets, authentication, or
anonymization, and creation times are activity metadata. The directory,
snapshot, and coordination file therefore use private permissions.

Every read validates the complete schema and every entry before returning
anything. An unknown schema, duplicate targets, malformed entries, or an
over-capacity list refuses the snapshot as a whole. Mutation does not salvage,
partially load, or overwrite corrupt state. Successful replacement is atomic
and serialized across processes.

### Surface boundary

The CLI adds, removes, and lists canonical targets. It does no history scan.
The HUD toggles a selected target and resolves the saved list on demand; `b` is
the exact-record shortcut, while `/bookmark` names any scope and `/bookmarks`
opens recall.

MCP exposes neither bookmark mutation nor this machine's local bookmark list in
the initial contract. Bookmark state expresses local user intent, while MCP
clients already receive canonical identity fields they can store under their
own policy. A future MCP surface would need an explicit local-state authority
and privacy review rather than inheriting CLI access accidentally.

Bookmark persistence is the only write introduced here; during discovery,
resolution, and detail display, all source stores remain read-only.

## Consequences

Users can keep a small durable collection across TUI sessions and manage the
same targets headlessly without duplicating prompt bodies. Missing stores and
conservative identity gaps appear honestly as unresolved entries, not silently
rewritten bookmarks.

The collection is intentionally not a tag database, sync protocol, export
format, or source-store annotation. It has a fixed capacity and one global
creation-ordered list. Content and thread recall choose a current
representative rather than promising physical continuity; exact continuity
requires a record bookmark and its content validator.
1 change: 1 addition & 0 deletions docs/dev/adr/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ multiple adapters or public payloads.
0013-pluggable-tui-layouts-and-workflows
0014-result-order-limit-and-streaming-merge
0015-deterministic-record-identity
0016-durable-bookmarks
0020-progressive-deep-search
0021-prompt-guided-conversation-routing
```
37 changes: 37 additions & 0 deletions docs/tui/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,43 @@ sequence outright; iTerm2, Ghostty, kitty, WezTerm and Alacritty accept
it. If a paste comes back stale, that is where to look first.
:::

(tui-bookmarks)=

## Bookmarks

The HUD can save and reopen private pointers to selected results. With the
results list or detail pane focused, press `b` to toggle an exact record
bookmark. A saved exact record shows `★` beside its `Record:` handle. The key is
focus-safe: while the search bar or filter owns focus, `b` remains ordinary
input.

Slash commands expose all three canonical scopes:

- `/bookmark` and `/bookmark record` toggle the selected logical occurrence.
- `/bookmark thread` toggles its conversation thread when the backend exposes
one.
- `/bookmark content` toggles the content-equality handle shared by equivalent
records.

Record and thread bookmarks are unavailable when the selected result does not
carry that identity; content identity is always available. Bookmark changes
touch only agentgrep-owned state. Every agent history source remains read-only.
See {ref}`the CLI bookmark guide <cli-bookmark>` for the scope and persistence
contract.

Run `/bookmarks` to scan the current stores and open the compact recall list.
Each row says `resolved` or `unresolved`; a missing target stays saved instead
of being discarded. Type in the filter to narrow the list, use the arrow keys
to move, and press `Enter` to reopen the highlighted record in detail. `Esc`
closes the list. `Ctrl-C` clears a non-empty filter first, then closes it.

Recall does not replace the loaded search results or the sticky filter. A
record bookmark reopens only the matching occurrence whose content validator
also agrees. A thread bookmark opens a representative record from that thread,
and a content bookmark opens an equal-content occurrence. Which representative
is available can change with the current stores; unresolved bookmarks can
resolve on a later visit.

## Completion

Both the search bar and the in-list filter offer
Expand Down
6 changes: 6 additions & 0 deletions src/agentgrep/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,8 @@ def main(argv: cabc.Sequence[str] | None = None) -> int:
parsed = parse_args(argv)
if parsed is None:
return 0
if isinstance(parsed, BookmarkArgs):
return run_bookmark_command(parsed)
if isinstance(parsed, GrepArgs):
return run_grep_command(parsed)
if isinstance(parsed, SearchArgs):
Expand Down Expand Up @@ -561,6 +563,7 @@ def main(argv: cabc.Sequence[str] | None = None) -> int:
should_enable_help_color,
)
from agentgrep.cli.parser import ( # noqa: E402 (re-exports must follow main definition)
BookmarkArgs,
CaseMode,
FindArgs,
FindPatternMode,
Expand All @@ -587,6 +590,7 @@ def main(argv: cabc.Sequence[str] | None = None) -> int:
format_grep_record,
print_find_results,
print_grep_results,
run_bookmark_command,
run_find_command,
run_grep_command,
run_search_command,
Expand Down Expand Up @@ -636,6 +640,7 @@ def main(argv: cabc.Sequence[str] | None = None) -> int:
"AnsiHelpTheme",
"AnswerNowInputListener",
"BackendSelection",
"BookmarkArgs",
"CaseMode",
"ColorMode",
"ConsoleSearchProgress",
Expand Down Expand Up @@ -880,6 +885,7 @@ def main(argv: cabc.Sequence[str] | None = None) -> int:
"record_matches_scope",
"resolve_codex_sqlite_root",
"resolve_env_root",
"run_bookmark_command",
"run_find_command",
"run_find_query",
"run_grep_command",
Expand Down
Loading
Loading