ragrig-gpui 0.5.0

Reusable GPUI components for Ragrig RAG chat, citations, and reference ingestion
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# ragrig-gpui — port plan

Status: M0 spike and M1 primitives complete (2026-10-17). M2 is next.
Date: 2026-10-17.

## 1. Starting point

- `ragrig-egui` 0.5.0 is 3,045 lines in nine modules: `chat_view`,
  `composer`, `attachments`, `sessions`, `agent_bar`, `dock`, `form`,
  `markdown`, and a small `lib.rs`. It is deliberately egui-only: no
  `ragrig`, no runtime; the host owns messages, input, and worker state.
- `ragrig-cli` 1.5.0 is a 5,257-line REPL plus `search.rs` (207),
  `grobid.rs` (887) and `cite.rs` (939). Its command surface is the
  specification for the demo: queries, attach/download, paper search,
  sessions, chat/embed/memory/parser/chunker hot-swap, profiles, corpora,
  BibTeX/GROBID, logs.
- `ragrig` 1.1.x is async, built on `tokio` (full) and `reqwest` 0.13.
  Its trait objects are `Send + Sync`, so an `AgentSession` can live in a
  dedicated Tokio actor. Progress and cancellation are synchronous callback
  traits (`Progress`, `Cancel`, `CancellationToken`).
- `ragrig-gpui` contains the M0 spike, the M1 primitive library and widget
  gallery, and the plan below. The crate names `ragrig-gpui` and
  `scico` are free on crates.io.

## 2. What changed in the GPUI ecosystem

There are two incompatible GPUI lines on crates.io:

1. **Official `gpui` 0.2.2** (Zed, published 2025-10-22, edition 2024).
   `gpui-component` 0.5.1 (2026-02-05) is the last component line built on it.
2. **`gpui-pre` 0.3.9** (a published snapshot of Zed, 2026-10-12) used by
   the current **`gpui-base` 0.7.3 / `gpui-component` 0.7.3 / `gpui-kit`
   0.7.3** stack (GPUI Kit, Longbridge).

The current stack is substantially ahead: 75+ styled components, a
virtualized chat `MessageScroller` with tail-following and jump-to-latest, a
native Markdown `TextView`, a textarea with inline tokens and
`InputEvent::PressEnter { secondary, .. }`, `resizable`, `dock`, `table`,
`virtual_list`, `select`, `setting` pages, dialogs, sheets, popovers,
notifications, tooltips, and AccessKit semantics.

**Recommendation: `gpui-kit = "0.7.3"`.** It re-exports GPUI, base,
components, and Lucide assets through one facade. The library must use
`gpui_kit::*` (never import `gpui` directly) so the backend stays swappable.
The cost is dependence on a third-party published snapshot; pin the 0.7
minor, re-export `gpui_kit`, and document the compatibility contract.
The `gpui-fast` fork remains a host-level graph choice, not a feature of
this library.

Fallback if the spike fails: official `gpui` 0.2.2 + `gpui-component` 0.5.1.
That line is eight months old and lacks `MessageScroller`, the newer
`setting` framework, and several input improvements, so it is a fallback,
not the plan.

### Observed environment risk

This machine has `libxkbcommon.so.0` but neither `xkbcommon.pc` nor the
unversioned `libxkbcommon.so` linker symlink. The Rust crates compile, but
linking fails with `unable to find library -lxkbcommon`. Install
`libxkbcommon-dev` and `libxkbcommon-x11-dev` before building on Linux.
`fontconfig`, `wayland-client`, and `x11` are present. ALSA is only needed for
the optional `speech` feature.

## 3. Recommended architecture

### 3.1 Layers

```text
examples/gallery.rs (M1 widget gallery, no controller)
examples/ragchat.rs (M2 full demo: controller, library, sessions, settings)
        │
ragrig-gpui (library)
  model/          ChatState, messages, attachments, components, sessions, settings
  chat_view.rs    transcript + per-message TextViewState + source chips
  composer.rs     auto-growing input, send/stop events
  attachments.rs  attachment chips
  session_picker.rs, agent_bar.rs, side_panel.rs
  settings.rs     ChatSettings + SettingPage helper
  rag/            protocol, Tokio worker, RagController, RagChat, RagSettings
  references.rs   GROBID pre-pass + reference rows and hover copy actions
        │
gpui-kit 0.7.3 (gpui-pre + gpui-base + gpui-component + assets)
```

- `ragrig` is a default feature: the controller, adapters, sessions, indexing,
  and attachments are part of the normal build.
- `grobid` adds the BibTeX/GROBID path through `grobid-bibtex`.
- There is no `discovery` feature: arXiv and Semantic Scholar are deprecated.
- The M0 example builds with `--no-default-features` so the UI gate does not
  pay for the Ragrig dependency graph.

### 3.2 State ownership (hybrid)

- **Entities** own state that GPUI must retain: `ChatView` (messages +
  `MessageScrollerState`), `Composer` (`TextareaState` + busy flags),
  `SessionPicker`, `LibraryView`, `SettingsView`.
- **Plain builders** render host-owned data: `MessageBubble`, attachment
  chips, agent bar, section panel, source list rows, settings rows.
- Pure `ChatState::apply(ChatEvent)` and `LibraryState::apply(IndexEvent)`
  reducers carry the streaming/indexing semantics, so tests do not need a
  window and Scico can reuse the logic with its own view.
- `ChatView` is the batteries-included projection; `MessageBubble` is the
  escape hatch for hosts that own their transcript.

This keeps the egui crate's host-owned spirit without fighting GPUI's
retained entity model.

### 3.3 Runtime bridge

`ragrig` uses `tokio` + `reqwest`; GPUI's executor is not a Tokio reactor.
Do not run ragrig futures directly on GPUI's executor.

```text
GPUI thread                         dedicated std::thread
───────────                         ─────────────────────
Entity<RagController>               Tokio runtime (multi-thread)
  send RagCommand  ───────────────► actor owns AgentSession
  receive RagEvent ◄─────────────── emits tokens/progress/results
  update entities + notify           CancellationToken per job
```

- One long-running actor owns the `AgentSession`; a running operation takes
  the session out, runs in a spawned task, and returns it on completion.
  While busy, stateful commands are rejected; `Cancel`/`Status`/`Shutdown`
  are still accepted.
- Events travel over a bounded async channel and are drained on the GPUI
  side, batching tokens into one entity update per frame.
- The controller owns the Tokio runtime by default. An external
  `tokio::runtime::Handle` constructor can be added later if a host already
  has a runtime.

### 3.4 Chat and streaming

- `ChatState` is the source of truth; `ChatEvent` is
  `User`, `AssistantDelta(String)`, `AssistantDone { meta, sources }`,
  `Error`, `Cancelled`, `Clear`, `LoadTurns`.
- `ChatView` renders with `MessageScroller`:
  virtualized rows, `FollowMode::Tail`, a built-in jump-to-latest button,
  `Role::Log`, and `remeasure_items` for the streaming row.
- Assistant text uses `gpui_component::text::markdown(...)` (`TextView`).
  Keep both a mutable accumulation `String` and a cached `SharedString`
  rebuilt per token batch, so a frame does not clone/parse the whole answer.
- Batch tokens (~30–60 Hz) in the worker/controller. If live Markdown
  proves expensive, stream as plain text and switch to Markdown on
  completion; keep this behind one `StreamingRenderMode` switch.
- A cancelled or failed answer keeps any partial text and is marked
  `Stopped`/`Error`; metadata is attached only when the pipeline completes.

### 3.5 Layout and settings

- Main window: left `Library` panel, center chat, optional right
  `Sources`/`Settings` sheet, bottom `AgentBar`/status.
- Use `h_resizable` / `ResizablePanelGroup`; a thin `SidePanel` widget owns
  section headers and a horizontal collapse strip, not the resizing logic.
- Use the full `dock::DockArea` only when Scico needs tabs and
  nested splits. Porting the egui dock's drag internals would be reinvention.
- Settings use `Sheet` + `SettingPage` / `SettingGroup` / `SettingItem`.
  Appearance applies immediately; pipeline changes use an explicit Apply.
  Changing embedder or chunker warns that a reindex is required.

### 3.6 Composer and citations

`gpui-base`'s `TextareaState` already provides the behaviors the egui
composer implemented by hand:

- `auto_grow(1, 6)` for the six-row cap;
- `InputEvent::PressEnter { secondary, .. }` with the `secondary-enter`
  binding → Ctrl/⌘+Enter submits, plain Return inserts a newline;
- `InputEvent::Change` for enable/send state;
- `insert()`, `replace_all()`, `set_selected_range()`, `focus()` for
  programmatic insertion;
- `InlineToken` spans for atomic `@Key` citations, with click/hover
  callbacks — a direct fit for Scico's reference attach.

`Composer::append_text` inserts at the caret (or at the end, configurable)
and keeps focus. `Composer::append_citation` can use an inline token later.

## 4. Port mapping

| ragrig-egui | ragrig-gpui replacement |
|---|---|
| `chat_view::ChatView` | `ChatView` entity + `MessageBubble`; `MessageScroller`; `text::markdown` |
| `chat_view::MessageMeta` | plain `MessageMeta` + `SourceRef` list from `RagResponse.retrieved` |
| `composer::Composer` | `Composer` entity over `TextareaState`; send/stop; submit event |
| `composer::append` | `Composer::append_text` / `append_citation` using `insert` + focus |
| `attachments` | plain `Attachment` model + chip row; `Button`/`Tag` + tooltips |
| `sessions::SessionRow` | `SessionPicker` entity backed by `Select` / `DropdownMenu` |
| `agent_bar::AgentBar` | stateless `AgentBar` + `Popover` menus; status dots from theme tokens |
| `dock::SectionDock` | `SidePanel` section stack inside `ResizablePanelGroup` |
| `form::{setting_row,note,warning}` | `gpui_component::setting` (`SettingGroup`/`SettingItem`) |
| `markdown` (pulldown-cmark) | delete; use `gpui_component::text::markdown` |
| `ChatRole`, `MessageState`, `ComponentStatus`, `AttachmentKind` | keep as plain enums; add `ComponentKind` and `LibraryAction` enums |

## 5. Demo scope mapping from ragrig-cli

| CLI surface | GUI equivalent |
|---|---|
| any text | composer → chat |
| `/attach`, `/attach clear` | attachment chips + file prompt/drop; one-shot clear on send |
| `/download <url>` | "Add URL" action routed to a selected corpus |
| `/scholar`, `/arxiv`, `/get` | Discover view: search, results table, bulk download |
| `/refs` | source-list action "extract references" (feature-gated) |
| `/chat ...` | Settings → Chat (provider, model, params, context, prompt) |
| `/embed ...` | Settings → Embedding + Library index actions (index/update/purge/top-k/threshold) |
| `/memory ...` | Settings → Memory (strategy, model, rewrite prompt) |
| `/parser`, `/chunker` | Settings → Parsing/Chunking |
| `/prompt ...` | Settings → prompt file pickers |
| `/profile ...` | Profile menu: save/load/list (and delete) |
| `/corpus ...` | Library → corpora list, on/off, sync/rebuild/remove |
| `/bibtex ...` | Settings → Bibliography + composer `@key` tokens (feature `grobid`) |
| `/hist ...` | Session picker |
| `/log ...` | Activity sheet / log level; MVP shows operations and errors |
| `/help`, `exit` | Help sheet; window close |

No slash-command parser is needed. A command palette (`command` component)
can optionally expose the same actions to power users.

## 6. Milestones

Estimates are rough, single developer, excluding upstream surprises.

### M0 — Foundation spike (done, 2026-10-17)

Delivered in `examples/spike.rs` and `src/`:

- `DockSkin::dock_area` with a center chat panel, a left Library panel, and a
  right Settings panel (all built with `DockLayout::tabs()`).
- Live Markdown `TextView` rows inside a tail-following `MessageScroller`.
- Auto-growing `Textarea` (`auto_grow(1, 6)`); Ctrl/⌘+Enter submits, Return
  inserts a newline.
- Fake token stream on a dedicated Tokio thread, unbounded `async-channel`,
  `cx.spawn` bridge, ~30 Hz batches, generation guard, and `CancelFlag` stop
  that keeps the partial answer.
- `Settings` page with dropdown, number, switch, and input fields; `Popover`
  agent menu; Library placeholder.
- 5 example tests, 1 library test, 1 doctest, plus an ignored typing benchmark.

**Gate: passed.**

Measured on this machine (release, 1600×1000 window):

| Metric | Release | Debug |
|---|---|---|
| 444-char answer, 18 batches | 562 ms | not measured end-to-end |
| Token batch lag (worker send → UI handler) | 40–80 µs | 200–260 µs |
| 500 simulated keystrokes | 72 ms (144 µs/char) | 913 ms (1.83 ms/char) |
| Cold example build (deps included) | 49.8 s | not measured |
| Warm rebuild | 1.2 s | 1.5 s |

The typing numbers include the test harness's per-key `run_until_parked`, so
they are upper bounds, not paint latency.

**Findings and consequences for M1**

- Linux links `-lxkbcommon` and `-lxkbcommon-x11`; the runtime `.so.0` files
  are not enough. `libxkbcommon-dev` and `libxkbcommon-x11-dev` are required.
- `gpui-kit`'s optional `gpui-fast` branch depends on `cocoa =0.26.0` while
  `gpui-pre` resolves `cocoa 0.26.1`. A fresh resolution picks 0.26.0 and
  succeeds; a stale lockfile fails. Keep `Cargo.lock` and document the pin.
  This is a real risk for consumers with an existing workspace lockfile.
- Declared MSRV is 1.88 because `ragrig` requires it. `gpui-kit` declares no
  MSRV; only Rust 1.99 has been tested. Do not claim support below 1.99 yet.
- GPUI's test scheduler rejects foreground wakeups from other threads, so the
  Tokio bridge is tested by a plain worker test. Keep controller tests outside
  the GPUI scheduler and reserve `#[gpui_kit::test]` for input and reducers.
- Rebuilding Markdown from a growing `String` re-parses the full message per
  batch and displays an unclosed code fence as a code block mid-stream. M1
  must own an `Entity<TextViewState>` per assistant message and use
  `TextViewState::push_str` (or a stabilized streaming tail).
- `WindowBounds::Maximized` behaved like a windowed restore size on KWin/X11;
  explicit bounds are reliable. The UI scale on this HiDPI X11 setup maps
  logical to roughly physical pixels.
- Not covered in M0: `Select`, file dialog/drop, resizable split, and dock
  layout persistence. `Select` is straightforward; the dialog/drop and
  persistence land with the library work.

### M1 — Primitive port, mock data (done, 2026-10-17)

Delivered:

- `model/`: `ChatRole`, `MessageState`, `ChatState`, `MessageMeta`, `SourceRef`,
  `NewMessage`, `Attachment`, `ComponentKind`/`ComponentStatus`, `SessionEntry`,
  `ChatSettings`; all reducers are pure and unit-tested.
- `ChatView`: tail-following transcript with one `Entity<TextViewState>` per
  message, incremental `push_str`, final fence stabilization, streaming dot,
  stopped/error states, metadata footer, source chips, copy and retry events.
- `Composer`: auto-grow input, Ctrl/⌘+Enter submit, Return newline, Send/Stop
  events, draft/focus/clear helpers, streaming and disabled state.
- `AttachmentRows`, `SessionPicker`, `AgentBar` with per-component popovers,
  `SidePanel`, and `chat_settings` / `chat_settings_page`.
- `init(cx)`, `prelude`, `#![warn(missing_docs)]`, no `unsafe`.
- `examples/gallery.rs`: seeded complete/stopped/error transcript, live mock
  stream, two-tab left and right docks, registered dock panels, settings and
  component views.
- 31 library tests, 2 gallery tests, 5 active spike tests + 1 ignored
  benchmark, 2 doctests. Clippy `-D warnings` clean.

Deviations and findings:

- A true inline streaming caret would require modifying the Markdown text. The
  view uses a blinking status dot plus a `streaming` label instead; the text
  stays clean for selection and copy.
- Streaming bodies stay raw in the `TextViewState`; `finish`/`stop` apply
  `stabilize_markdown` once, so an open fence cannot swallow later tokens.
- The GPUI test scheduler cannot advance a foreign worker thread, so the
  gallery's mock stream uses GPUI timers and deterministic `advance_clock`
  tests. The M0 Tokio worker test remains outside the GPUI scheduler.
- `CancelFlag` is not wired to the gallery: there is no controller yet. The
  Composer's `Stop` event is the seam M2 will connect to
  `CancelFlag`/`CancellationToken`.
- Dock panels are registered, but the gallery does not load or save layouts;
  M3 adds `.ragrig/gpui.json` persistence.

### M2 — Controller and RAG chat demo (done, 2026-10-17)

Delivered:

- `src/rag/`: `RagCommand`/`RagEvent` protocol, dedicated multi-thread Tokio
  worker, `RagController` entity, full `RagSettings` page, and the composite
  `RagChat` surface (transcript + attachments + composer).
- Streaming tokens are batched on the worker (~30 Hz) and delivered through a
  bounded `async_channel`; `Stop` cancels through Ragrig's
  `CancellationToken` and keeps the partial answer. Non-streaming mode is a
  setting.
- Agent construction from `RagrigConfig` (chat, embedder, parser filter,
  chunking, memory strategy, system prompt). DeepSeek keys are read from
  `DEEPSEEK_API_KEY` only and never enter a profile.
- Sessions: list, load, new, auto-save after every successful turn; Apply
  rebuilds the agent and reloads the current session when it was saved.
- Library: incremental folder ingestion with progress, cancel, document list,
  path resolution, and one-shot file attachments on the next question.
- Settings: workspace, provider, models, temperature/tokens, prompt, embeddings,
  retrieval, PDF backend, chunking, memory, profiles (`name=path` corpora are
  kept across applies).
- `examples/ragchat.rs`: four dock panels (Chat, Library, Sessions, Settings,
  Components), onboarding banner, agent bar, session picker, status line, and
  `RAGRIG_WORKSPACE` / `RAGRIG_CORPUS` / `RAGRIG_MODEL` /
  `RAGRIG_EMBED_MODEL` / `RAGRIG_ASK` environment hooks.
- 44 library tests (including six actor tests with a fake generator and
  embedder), 2 gallery tests, 5 active spike tests, 2 doctests; clippy clean.

Acceptance: verified manually against Ollama with `llama3.2:3b` and
`nomic-embed-text`: indexed a Markdown corpus (1 document, 7 chunks), streamed
an answer with `7 chunks · 1 source · 5.1 s`, displayed the source chip, and
reloaded the saved session from the picker. An empty workspace streamed a pure
chat answer with `0 documents · 0 chunks`. The 3.3 MB PDF fixture parsed and
indexed concurrently (4,757 chunks, progress visible, interruptible).

Findings:

- The initial worker path never sent `AgentReady` (only `Apply` did). The
  controller therefore stayed on "Starting the RAG worker…". Unified the
  announcement after any build.
- `BruteForceStore` deep-copies on clone in some backends: ingesting through a
  cloned `RagAgent` left the session's store empty. Ingestion now holds the
  session lock and runs on the session's own agent; generation requests wait
  for indexing to finish.
- `ChatView::remove` was added so retry can drop the failed message before
  resending its prompt without echoing the user turn.
- Widget callbacks that mutate their own entity must be deferred
  (`cx.defer`); the retry path does this to avoid a re-entrant update.

Release: **0.2.0**.

### M3 — Library management (4–6 days)

- Corpora (dir/URL), add/remove/toggle, sync/rebuild/purge, progress and
  cancel, per-document rows from `stored_documents()`, filter/search.
- Add files/folder via path prompt and drop; download URL; attach a
  library document to the next query; delete/reindex/reveal row actions.
- Pending/changed detection with `get_changed_documents` where available.
- Profiles save/load/list; workspace `.ragrig` compatibility; UI layout
  persistence in `.ragrig/gpui.json`; secrets never persisted.
- Acceptance: two corpora, incremental update, remove one corpus, attach
  a document, restart and recover state.

### M4 — GROBID and bibliography (done, 2026-10-17)

Delivered before M3 at the user's request. The version stays at 0.2.0;
0.3.0 remains the next planned bump after M3.

- `grobid` feature (implies `ragrig`): depends on `grobid-bibtex` 0.3 with
  its `pipeline` feature (which includes OpenAlex completion). `biblatex` is
  a direct dependency only to read entry fields.
- `src/references.rs`: the full GROBID pre-pass (fingerprint manifest, extract,
  optional OpenAlex completion, optional rename, merge) adapted to UI rows,
  plus progress/report types, PDF path resolution, and the hover row.
- Reference worker commands/events in the existing controller: extract,
  stop, refresh, progress, loaded rows, report. The bibliography lives at
  `<workspace>/.ragrig/references.bib` and every entry links its PDF.
- `ReferenceRow`: two-line reference row with **Copy @key** (writes `@key`)
  and **Copy path** (writes the resolved absolute PDF path). Both controls
  are laid out while hidden so the row does not jump; a missing PDF keeps the
  path button disabled with an explanatory tooltip.
- Demo Library panel: a References section above Documents with count, GROBID
  parse button, refresh, progress, last-run report, bibliography path, and
  the rows. `RAGRIG_GROBID=1` parses the corpus/workspace on startup.
- Settings: GROBID URL, rename switch (off by default), OpenAlex switch (off
  by default). Applying chat settings never turns these on implicitly.
- 50 active library tests plus one ignored GROBID end-to-end test; the hover
  test drives a real GPUI frame and asserts both clipboard contents.

Acceptance: against the running GROBID server, the three PDFs in
`test_library/` produced three entries with keys `Kano2025`, `Kobayashi2011`,
and `Kozmik2005`, merged with `file` links. A second run reported three cached
records and three duplicates. The ignored test also runs a rename pass on a
copy of the corpus and verifies that every merged row resolves to its renamed
PDF.

Findings:

- `grobid-bibtex` 0.3's pre-pass already owns fingerprinting, caching,
  completion, renaming and duplicate detection; reimplementing any of it in
  the UI would have duplicated upstream work.
- `PrepassObserver` is synchronous and does not require `Send`, so the
  observer's closures need explicit `Send`/`Sync` bounds for the extraction
  future to run on a Tokio worker.
- `Collection::iter()` yields `biblatex::Entry`; reading fields needs the
  `ChunksExt` trait, hence the direct (optional) `biblatex` dependency.
- GROBID links absolute paths in this setup; relative paths are resolved
  against the scanned corpus, with a keyed-filename fallback.
- Inline `@key` resolution was deferred to M5; the deprecated arXiv/Semantic
  Scholar discovery remains out.

### M5 — Roundtrip Experience (done, 2026-10-17)

Delivered at the user's request before M3. The version stays at 0.2.0;
0.3.0 remains the next planned bump after M3.

- **Inline citations.** `grobid-bibtex`'s `cite_keys` scans the question; the
  worker resolves keys against `<workspace>/.ragrig/references.bib` and the
  active corpus folders. The recorded `file` field wins, then a keyed file
  name, quoted aliases resolve to the canonical key, and misses never abort
  the question. Up to 3 papers per query, 100,000 characters each; a document
  cited by two aliases is attached once.
- The worker caches a `CiteState` across questions (the upstream type refreshes
  itself when the bibliography's size or mtime changes). Resolution runs on
  Tokio's blocking pool before generation, so no PDF parsing touches GPUI.
- `RagEvent::Citations` carries the attached keys and miss lines. `RagChat`
  inserts a system note *before* the assistant placeholder, giving the
  Scico order: question, `Attached: @Key`, then the streaming answer.
- `RagSettings::resolve_citations` (default **on**) is exposed as **Use @key
  citations** in Settings → References and threaded through `RagSetup`.
- **Prompt context fix.** `RagAgent::builder().system_prompt` trusts the caller
  to include `{context}`, while `RagAgent::set_system_prompt` appends it. The
  controller now applies prompts through the latter, so a plain prompt no
  longer silently drops retrieval and attachments. A regression test asserts
  that a prompt without the placeholder still receives an attached paper.
- **Scico context containers.** `ScicoAttach` replaces the generic
  `[Attached: …]` format with the co-writer prompt's
  `<scico:v1:context source="creator">` / `source="corpus"`
  containers, so an attached `@Key` paper is visibly a document, not a
  retrieval miss.
- **Context budget.** The strategy receives the configured `context_tokens`,
  gives 70% of the block to explicit attachments and the rest to retrieval,
  and adds `<scico:v1:truncated shown="…" total="…"/>` to shortened
  papers. Without this, a 36k-char cited PDF plus 9k chars of retrieval
  overflowed a 4096-token model and Ollama dropped the attachment.
- A folder parsed with GROBID joins the citation corpora even when it was not
  indexed, so references extracted from a folder are immediately citable.
- **Multi-select.** `ReferenceSelection` stores keys, not indices, so a
  refreshed list keeps the selection and drops vanished rows.
  `ReferenceRow::on_select` adds the large leading **+** (revealed on hover,
  replaced by a check when selected, tinted row background).
- `ReferenceSelectionBar` is one floating pill for the list: selected count,
  **Copy @keys**, **Copy paths**, **Attach to next question** and **Clear**.
  The demo mounts it in the Library panel footer, where it stays visible while
  the list scrolls. Copy actions write the clipboard; attach and clear are
  reported to the host, which owns the chat and selection.
- 69 active library tests, including citation resolution (recorded file,
  keyed fallback, aliases, cap, truncation, missing bibliography), worker
  roundtrips that prove two multi-selected files reach the prompt with and
  without a custom placeholder, context-container formatting and budgeting,
  and GPUI tests for the hover **+**, toggling, and both bulk clipboard
  actions.

Acceptance: with the running GROBID server, the ignored end-to-end test
produced the three test-library records and resolved the first key through the
real PDF parser. In the demo, a question containing `@Kano2025` showed
`Attached: @Kano2025` above the answer. After the prompt and budgeting fixes,
llama3.2:3b summarised the attached paper from its body text even with a
4096-token context, and two multi-selected files arrive as two creator
containers.

### M6 — Hardening and publication (3–5 days)

- Accessibility: labels for icon buttons, focus order, log role,
  reduced-motion busy pulse.
- Performance: large transcript, long answer, virtual list, token batching,
  no per-frame full-text clones; release-profile checks.
- Docs: README (GPUI Kit pin, quick start, compatibility), Changelog,
  examples; verify `cargo package --list` contains the intended example(s).
- Green checks: fmt, check all targets/features, test, clippy `-D warnings`,
  doc, package, publish dry-run. E2E with Ollama/GROBID remains manual or
  nightly.
- Acceptance: demo works on Linux; macOS/Windows smoke-tested; package
  metadata complete.

## 7. Decisions (approved)

| # | Decision |
|---|---|
| D1 | **`gpui-kit = "0.7.3"`** as the single facade; the library never imports `gpui` directly. Fallback: official `gpui 0.2.2` + `gpui-component 0.5.1`. |
| D2 | **`ragrig` is a default feature**; the controller lives in the library and the example uses it from M2. |
| D3 | Tiered releases: **0.2.0 after M2**, **0.3.0 after M3**. |
| D4 | **Single crate + example**: no workspace member; `examples/spike.rs` (M0), `examples/gallery.rs` (M1), `examples/ragchat.rs` from M2. |
| D5 | **Hybrid state**: entities for chat/composer/pickers/library/settings; plain builders for rows, chips, and the agent bar; pure reducers. |
| D6 | **Controller in the library**; no slash-command parser. |
| D7 | **Dedicated Tokio runtime thread**; one actor owns `AgentSession`; bounded events; batched tokens; `CancellationToken`. |
| D8 | **`DockArea`** for the demo because Scico will use tabs. |
| D9 | **Secrets from the environment only** (`DEEPSEEK_API_KEY`); never persisted. |
| D10 | **M4 is GROBID/BibTeX only**; arXiv and Semantic Scholar are deprecated. GROBID logic comes from `grobid-bibtex`. |
| D11 | **M5 is Roundtrip Experience** — `@key` prompt attachment and reference multi-select. Hardening/publication moved to M6. |

Defaults carried into M1: `MessageScroller` + incremental Markdown,
keep partial answers on stop, `Settings` pages, Lucide via
`gpui-kit-assets`, manual Ollama/GROBID E2E.

Resolved by the user or by D10:

1. No paper discovery (arXiv/Semantic Scholar) in any release.
2. GROBID/BibTeX is the final milestone, not part of the first release.

Defaults for the remaining open questions (say so if any should change):

1. Profiles stay CLI-compatible; secrets never enter profiles.
2. One window with dock panels; no separate library/settings windows.
3. No offline fixture mode in the first pass.
4. Buttons/menus only; no command palette.
5. Publish primitives + `RagChatPanel`, not a turnkey app.
6. Linux first; macOS/Windows smoke tests at M5.

## 8. Risks and mitigations

| Risk | Mitigation |
|---|---|
| `gpui-pre` snapshot churn | Pin `gpui-kit = "0.7.3"`/0.7.x; re-export it; keep widgets thin; upgrade deliberately |
| Link failure on Linux | Install `libxkbcommon-dev` and `libxkbcommon-x11-dev`; runtime `.so.0` files are insufficient. Documented in README |
| `cocoa` resolution conflict on macOS | `gpui-fast` pins `cocoa =0.26.0`; keep a fresh `Cargo.lock`, and warn consumers whose workspace locks 0.26.1. Re-evaluate before publishing; fall back to official `gpui` if it persists |
| `gpui-fast` changes type identity | It is a backend choice for the host, not a feature of this crate; never import `gpui` directly |
| Live Markdown re-parses per batch | M1: one `TextViewState` per message with `push_str`; measure again; keep a plain-text streaming fallback |
| Unclosed code fences mid-stream | M1: stabilize the streaming tail (close fences, hide the trailing partial block) |
| Virtual list remeasure on streaming row | `remeasure_items` only the last row; test long answers |
| Tokio/GPUI bridge deadlocks | Single actor; bounded channel; batching on the GPUI side; cancellation token |
| GPUI test scheduler rejects cross-thread wakes | Test controller/worker code outside `#[gpui_kit::test]`; keep UI tests deterministic |
| `WindowBounds::Maximized` ignored by some WMs | Use explicit bounds and verify on the target platform |
| CLI parity is large | Tiered scope; controller reuse; no command-parser port |
| Secrets in profiles | Scrub/skip keys; env/session only |
| GROBID is fresh (0.2.1, published 2026-10-16) | Feature-gate; depend on crates.io, no path; defer if unstable |
| Compile time / binary size | Dev profile opt-level for deps; no unnecessary tree-sitter features |

## 9. Explicit non-goals for the port

- Do not port the custom `pulldown-cmark` renderer. Use `TextView`.
- Do not port the egui Markdown `Block` model.
- Do not port panel resizing or section fold logic. Use `DockArea` and
  `ResizablePanelGroup`.
- Do not port form helpers beyond a thin compatibility shim. Use `setting`.
- Do not port icons or theme colors. Use `gpui-kit-assets` and semantic tokens.
- Do not build an editor. The composer is the only text input.
- Do not emulate slash commands. UI actions replace them.
- Do not depend on GPUI under the name `gpui` anywhere in the library.

## 10. Validation commands

```sh
cargo fmt --check
cargo check --all-targets --all-features
cargo test --all-features
cargo clippy --all-targets --all-features -- -D warnings
cargo doc --no-deps --all-features
cargo run --example spike --no-default-features
```

On this machine, prepend the local link shim if the dev packages are not
installed:

```sh
RUSTFLAGS="-L native=$PWD/target/xkb-shim" cargo check --all-targets --all-features
```

The shim is a development aid, not part of the crate. `libxkbcommon-dev` and
`libxkbcommon-x11-dev` are the supported setup.

Feature matrix:

```sh
# UI only, the M0 gate
cargo test --no-default-features

# default: controller dependency included
cargo test

# all optional integrations (ragrig + grobid)
cargo test --all-features
```

There is no `discovery` feature. Do not add a `gpui-fast` feature to this
crate: it changes the backend for the whole dependency graph.

## 11. Next implementation step

M3 remains the open functional milestone: deepen the library with
enable/remove/sync/rebuild/purge, per-document chunk counts and actions,
drag/drop paths, pending/change detection, and dock layout persistence in
`.ragrig/gpui.json`. After that, M6 is the hardening and publication pass.
Apply the same worker protocol; do not move Ragrig futures onto GPUI's
executor.