rab-agent 0.1.5

rab is a lightweight, extensible, Rust-based coding agent.
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
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
# rab Architecture

A lightweight, extensible Rust coding agent inspired by [pi-coding-agent](https://pi.dev).
rab delegates the core agent loop, types, and provider layer to the **yoagent** crate,
providing the session layer, TUI, built-in tools, slash commands, file search tools (grep/find/ls),
file mutation queue, and lifecycle management.

---

## Layered architecture

```
┌──────────────────────────────────────────────────────────────────────┐
│                          rab (EPL-2.0)                               │
│                                                                      │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │                    main.rs (manual arg parsing)                │   │
│  │  arg parsing, env reading, session init,                      │   │
│  │  mode dispatch (print / interactive), extension gating,       │   │
│  │  context file loading, skills, auth                            │   │
│  └────────────────────┬─────────────────────────────────────────┘   │
│                       │                                              │
│  ┌────────────────────▼─────────────────────────────────────────┐   │
│  │              AgentSession (agent_session.rs)                  │   │
│  │  Primary entry point; owns Session + config (cwd, dir,       │   │
│  │  persist, lazy write). Factory methods: create, open, etc.  │   │
│  │  - Event-driven message persistence (crash-safe)             │   │
│  │  - Model/thinking/tool change detection & recording          │   │
│  │  - Auto/manual compaction (compaction.rs)                    │   │
│  │  - Branch summarization (branch_summary.rs)                  │   │
│  │  - Branch navigation (set_branch)                            │   │
│  │  - Pi-compatible persist_message_end pattern                 │   │
│  └──────────┬───────────────────────────────────────────────────┘   │
│             │                                                       │
│  ┌──────────▼───────────────────────────────────────────────────┐   │
│  │              Session (session.rs)                             │   │
│  │  High-level API wrapping SessionStorage. Pi-compatible:      │   │
│  │  append_*, build_context, move_to, metadata.                 │   │
│  └──────────┬───────────────────────────────────────────────────┘   │
│             │                                                       │
│  ┌──────────▼───────────────────────────────────────────────────┐   │
│  │              SessionStorage (session_storage.rs) trait        │   │
│  │  Low-level CRUD: leaf mgmt, labels, path queries.            │   │
│  │  Impls: InMemorySessionStorage, JsonlSessionStorage          │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                                                                      │
│  ┌────▼──┐ ┌────▼──┐ ┌────▼──┐ ┌────▼──┐ ┌────▼──┐ ┌────▼──┐      │
│  │builtin│ │  tui/  │ │commands│ │extens-│ │settings│ │ auth  │      │
│  │read   │ │ agent/ │ │.rs     │ │ions/  │ │.rs     │ │.rs    │      │
│  │write  │ │ ui/    │ │22 slash│ ┌──────────────┐ │~/.rab/ │ │API    │
│  │edit   │ │screen  │ │commands│ │file_search (3)│ │settings│ │keys,  │
│  │bash   │ │editor  │ │        │ │mcp/ (6 mods,  │ │AGENTS  │ │OAuth  │
│  │file_  │ │list    │ │        │ │ 2K lines)     │ │.md     │ │       │
│  │mutation│ └───────┘ │        │ │AGENTS.md       │ │skills  │ │       │
│  │_queue │            │        │ │skills          │ │        │ │       │
│  │cancel │            │        │ │                │ │        │ │       │
│  └──┬────┘            │        │ └──────────────┘ └───────┘ └───────┘      │
│     │                 │        │                                     │
│     │     impl Extension trait + yoagent::types::AgentTool          │
│     │                                                               │
│  ┌──▼──────────────────────────────────────────────────────────┐   │
│  │              agent/extension.rs (Extension trait)             │   │
│  │  pub trait Extension: Send + Sync {                          │   │
│  │    fn name(&self) -> Cow<'static, str>;                      │   │
│  │    fn tools(&self) -> Vec<ToolDefinition>;                   │   │
│  │    fn commands(&self) -> Vec<SlashCommand>;                  │   │
│  │    fn skills(&self) -> SkillSet;                             │   │
│  │  }                                                           │   │
│  │  ToolDefinition wraps AgentTool with: snippet, guidelines,   │   │
│  │  prepare_arguments, before_tool_call hook, after_tool_call   │   │
│  │  hook, and bundled ToolRenderer.                             │   │
│  │  validate_tool_arguments() + coerce_with_json_schema()       │   │
│  │  Builtin + user extensions share this trait                  │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                       │                                              │
│  ┌────────────────────▼─────────────────────────────────────────┐   │
│  │                   yoagent 0.8.4 (MIT)                        │   │
│  │                                                              │   │
│  │  ┌──────────────────────────────────────────────────────┐   │   │
│  │  │           yoagent::types                             │   │   │
│  │  │  AgentMessage, Message (User/Assistant/ToolResult),   │   │   │
│  │  │  Content, AgentTool, AgentEvent, Usage, etc.          │   │   │
│  │  └──────────────────────────────────────────────────────┘   │   │
│  │  ┌──────────────────────────────────────────────────────┐   │   │
│  │  │           yoagent::provider                          │   │   │
│  │  │  Provider trait + OpenAiCompatProvider               │   │   │
│  │  │  Streaming LLM calls, thinking, tool calls            │   │   │
│  │  │  Provider auto-detection by model prefix              │   │   │
│  │  └──────────────────────────────────────────────────────┘   │   │
│  │  ┌──────────────────────────────────────────────────────┐   │   │
│  │  │           yoagent::agent                             │   │   │
│  │  │  Agent struct, run_agent_loop(),                     │   │   │
│  │  │  text/tool streaming, event emission                 │   │   │
│  │  └──────────────────────────────────────────────────────┘   │   │
│  │  ┌──────────────────────────────────────────────────────┐   │   │
│  │  │           yoagent::skills                            │   │   │
│  │  │  Skill type, frontmatter parsing                     │   │   │
│  │  └──────────────────────────────────────────────────────┘   │   │
│  │                                                              │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                       │                                              │
│  ┌────────────────────▼─────────────────────────────────────────┐   │
│  │  Provider backends (in yoagent, not rab)                     │   │
│  │  OpenCode Go (opencode.ai/zen/go/v1) — default               │   │
│  │  OpenAI, Anthropic, Ollama — auto-detected by model prefix   │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                                                                      │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  TUI (src/tui/ + src/agent/ui/) — 50+ modules, ~630 tests   │   │
│  │  Direct Rust port on crossterm 0.29                          │   │
│  └──────────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────────┘
```

## Key architectural decisions

- **yoagent is the core dependency**, not genai. rab delegates the agent loop,
  provider layer, and message types to yoagent. rab provides the session layer,
  TUI, built-in tools, file search tools, slash commands, and lifecycle
  management on top.

- **One extension mechanism** — built-in tools and user extensions use the same
  `Extension` trait. No separate tool registration path. All tools, commands,
  renderers, and skills go through `Extension`.

- **ToolDefinition wraps every tool** — each `AgentTool` is wrapped in a
  `ToolDefinition` that carries prompt snippet metadata, guidelines, argument
  preparation hooks (`prepare_arguments`), `before_tool_call` and
  `after_tool_call` hooks (pi-compatible), and automatic JSON Schema argument
  coercion + validation.

- **Pluggable operations** — every built-in tool (read, write, edit, bash,
  grep, find, ls) delegates filesystem/shell operations through a trait
  (e.g. `ReadOperations`, `BashOperations`, `GrepOperations`, `FindOperations`,
  `LsOperations`), making it possible to replace local execution with remote (SSH) execution.

- **Provider layer lives in yoagent** — rab has no `adapter.rs` or `provider.rs`.
  yoagent's `Provider` trait and `OpenAiCompatProvider` handle all LLM
  communication. OpenCode Go is the default backend; OpenAI, Anthropic, and
  Ollama are auto-detected by model name prefix.

- **Agent loop lives in yoagent** — rab has no `loop.rs`. yoagent's `Agent`
  struct handles streaming, tool execution, and event emission. rab subscribes
  to events via `AgentEvent` for persistence and UI updates. A fresh `Agent`
  is created per turn (new agent loop per user message), using yoagent's
  native `follow_up()` for mid-turn message queuing and `steer()` for
  turn-level message injection.

- **Types from yoagent**`AgentMessage`, `Message`, `Content`, `AgentTool`
  are all re-exported from `yoagent::types`. rab's `types.rs` is a thin shim
  with helper functions only (no rab-specific enums).

- **File mutation queue** — concurrent file writes/edits to the same file are
  serialized via `with_file_mutation_queue()` so the model can issue multiple
  sequential edits to the same file without races.

## Pi component mapping

| pi component | rab equivalent | Status |
|---|---|---|
| `pi-tui` (terminal UI, components, editor) | `src/tui/` + `src/agent/ui/` | ✅ Complete — 50+ modules, ~630 tests. Direct Rust port on crossterm 0.29. |
| `pi-agent-core` (agent loop, session, compaction, skills) | Delegated to **yoagent** (agent loop, types, provider, skills) + rab's `AgentSession` (session lifecycle, compaction, branching) | ✅ Agent loop in yoagent (`yoagent::agent::Agent`). ✅ Session in `session.rs` (~2865 lines). ✅ SessionStorage in `session_storage.rs` (~630 lines). ✅ Compaction in `compaction.rs` (~1140 lines). ✅ Branch summarization in `branch_summary.rs` (~440 lines). ✅ Skills loaded via `yoagent::skills::SkillSet`. |
| `coding-agent` (CLI, extensions, tools, settings, commands) | `main.rs`, `builtin/`, `extensions/`, `settings.rs`, `auth.rs`, `commands.rs` | ✅ Tools (read/write/edit/bash/grep/find/ls), settings, auth, CLI done. ✅ 22 slash commands implemented. ✅ Extension trait with tools, commands, renderers, skills, hooks. |
| `GrepTool`, `FindTool`, `LsTool` (pi agent tools) | `src/extensions/file_search.rs` | ✅ grep (ripgrep/grep fallback), find (fd/find fallback), ls — all with pluggable operations. |
| MCP adapter (pi-mcp-adapter) | `src/extensions/mcp/` (6 modules, ~2040 lines) | ✅ Proxy `mcp` tool, direct tool adapters, SSE-aware HTTP transport, config loading (global+project merge), server lifecycle (lazy connect, idle timeout), persistent metadata cache, tool renderers. |
| provider | `yoagent::provider::OpenAiCompatProvider` | ✅ OpenCode Go default. Auto-detection by model prefix (claude → Anthropic, gpt → OpenAI, ollama → Ollama). |
| `beforeToolCall` / `afterToolCall` | `ToolDefinition.before_tool_call` / `.after_tool_call` | ✅ Per-tool hooks for blocking/preprocessing/postprocessing |
| `validateToolArguments` | `extension::validate_tool_arguments()` | ✅ Full JSON Schema validation with pi-compatible error paths |
| Argument coercion | `extension::coerce_with_json_schema()` | ✅ Type coercion matching pi's `Value.Convert` + `coerceWithJsonSchema` |
| Theme system | `src/agent/ui/theme.rs` | ✅ JSON theme system with resolution, fallback, detection (715 lines) |
| Resource loading (AGENTS.md/CLAUDE.md) | `src/agent/context_files.rs` | ✅ AGENTS.md/CLAUDE.md discovery, `<project_context>` wrapping |
| Skills | `yoagent::skills::SkillSet` + `SystemPromptBuilder.skills()` | ✅ Skill loading, frontmatter, prompt formatting, /skill:name expansion |
| Image support (Kitty protocol) | `src/tui/components/markdown.rs` (hyperlinks) + base64 data URLs | ✅ Image display via Kitty protocol. Input (clipboard paste) TBD. |
| Config files | `~/.rab/` | ✅ Same schema as pi. Auth at `~/.rab/agent/auth.json`. |
| Footer data (git branch, extensions) | `src/agent/footer_data_provider.rs` | ✅ Git branch resolution (worktree/reftable support), extension statuses, provider count |
| File mutation queue | `src/builtin/file_mutation_queue.rs` | ✅ Per-file serialization using tokio::sync::Notify, same pattern as pi |
| MCP extension | `src/extensions/mcp/` | ✅ Proxy `mcp` tool, direct tools, config loading, server lifecycle, cache, renderers |
| WASM plugin system | Not started | ⬜ Phase 2 |

---

## Core type system (`src/agent/types.rs`)

A thin shim over `yoagent::types`. Provides helper functions and rab-specific enums.

### yoagent types (re-exported)

```rust
// From yoagent::types:
pub use yoagent::types::{AgentMessage, Content, Message};
// AgentMessage is an enum:
//   AgentMessage::Llm(Message) — user, assistant, tool_result
//   AgentMessage::Extension(...) — extension-specific data
// Message::User { content, timestamp, ... }
// Message::Assistant { content, model, provider, usage, error_message, ... }
// Message::ToolResult { tool_call_id, tool_name, content, is_error, ... }
// Content::Text { text }
// Content::ToolCall { id, name, arguments }
// Content::Thinking { text, signature }
```

### Helper functions

```rust
pub fn content_text(content: &[Content]) -> String;       // text parts joined
pub fn content_tool_calls(content: &[Content]) -> Vec<(id, name, args)>;
pub fn message_text(msg: &AgentMessage) -> String;
pub fn message_is_user(msg: &AgentMessage) -> bool;
pub fn message_is_assistant(msg: &AgentMessage) -> bool;
pub fn message_is_tool_result(msg: &AgentMessage) -> bool;
pub fn message_is_error(msg: &AgentMessage) -> bool;
pub fn message_tool_call_id(msg: &AgentMessage) -> Option<&str>;
pub fn message_usage(msg: &AgentMessage) -> Option<Usage>;
pub fn message_error(msg: &AgentMessage) -> Option<&str>;
pub fn message_tool_call_count(msg: &AgentMessage) -> usize;
pub fn user_message(text: &str) -> AgentMessage;
pub fn assistant_message(text: &str) -> AgentMessage;
pub fn tool_result_message(tool_call_id: &str, text: String, is_error: bool) -> AgentMessage;
```

---

## Agent lifecycle (`src/agent/agent_session.rs`)

The `AgentSession` struct is the primary entry point for session management.
It owns a `Session` directly (not `SessionManager`) plus app-level config.
Factory methods (`create`, `open`, `in_memory`, etc.) replace the
`SessionManager::create()` pattern.

```rust
pub struct AgentSession {
    session: Session,
    session_dir: PathBuf,
    cwd: PathBuf,
    persist: bool,
    flushed: bool,                           // lazy-write state
    last_model: Option<(String, String)>,
    last_thinking_level: String,
    last_active_tools: Option<Vec<String>>,
    persisted_message_ids: HashSet<String>,
    persisted_tool_call_ids: HashSet<String>,
    compaction_settings: CompactionSettings,
    context_window: u64,
    model_name: String,
    compaction_api_key: Option<String>,
    model_config: Option<yoagent::provider::model::ModelConfig>,
    thinking_level: yoagent::types::ThinkingLevel,
    extensions: Vec<Box<dyn Extension>>,
    event_listeners: Vec<CompactionEventCallback>,
    overflow_recovery_attempted: bool,
}
```

### Responsibilities

1. **Factory methods**`AgentSession::create()`, `.open()`, `.in_memory()`,
   `.continue_recent()`, `.fork_from()` — create sessions directly.

2. **Lazy write**`ensure_flushed()` migrates from in-memory to file-backed
   storage on the first assistant message (pi-compatible deferred persistence).

3. **Event-driven persistence**`handle_yo_event()` persists tool results
   immediately (crash-safe). `on_agent_end()` persists remaining messages.

4. **Model/thinking/tool change tracking**`on_model_change()`,
   `on_thinking_level_change()`, `on_active_tools_change()` append metadata
   entries only when values differ from last known (diff-based).

5. **Auto-compaction**`check_auto_compact()` runs after the agent finishes
   a turn. Calls `compact()` to generate a summary.

6. **Manual compaction**`run_manual_compact()` for `/compact`.

7. **Branch summarization**`summarize_branch_navigation()` summarises
   abandoned branches. `set_branch()` moves the leaf pointer.

8. **New session**`new_session()` creates a fresh in-memory session.

### Typical usage in print mode

```rust
let mut agent_session = AgentSession::create(&cwd, session_dir.as_deref());
agent_session.set_compaction_config(api_key, &model, context_window);

// Submit user message
let msg = user_message("list .rs files");
agent_session.send_user_message_obj(&msg);

// Spawn yoagent agent loop
let agent = yoagent::agent::Agent::new(OpenAiCompatProvider)
    .with_model(&model)
    .with_api_key(&api_key)
    .with_system_prompt(&system_prompt)
    .with_tools(agent_tools);

agent.prompt_with_sender(msg_text, tx).await;

// Process events — AgentSession persists tool results immediately
while let Some(event) = rx.recv().await {
    agent_session.handle_yo_event(&event);
    // Update UI ...
}

// AgentEnd persists remaining messages
agent_session.check_auto_compact().await;
```

---

## Session layer (`src/agent/session.rs`) — ~2865 lines

Pi-compatible three-layer architecture:

```
AgentSession → Session → SessionStorage
                (high-level)  (low-level CRUD)
```

### Session (high-level wrapper)

Wraps `Box<dyn SessionStorage>` and provides pi-compatible entry construction,
context building, and branch navigation. All `append_*` methods generate typed
`SessionEntry` values with auto-generated IDs, parent chains, and timestamps.

```rust
pub struct Session {
    storage: Box<dyn SessionStorage>,
}
```

Key methods: `append_message()`, `append_model_change()`,
`append_thinking_level_change()`, `append_compaction()`,
`append_branch_summary()`, `append_label_change()`, `append_custom_entry()`,
`append_custom_message_entry()`, `build_context()`, `move_to()`,
`get_branch()`, `get_entries()`, `get_entry()`, `get_leaf_id()`,
`get_label()`, `get_session_name()`, `find_entries()`, `session_id()`,
`session_file()`, `session_name()`, `metadata()`, `set_leaf_id()`.

### SessionManager (internal helper)

`SessionManager` still exists as an internal helper behind the scenes.
It provides factory methods (`create`, `open`, `in_memory`, etc.) and
lazy-write tracking that `AgentSession` uses during construction.
Not in the public API path — `main.rs` and `App` use `AgentSession` directly.

### Format

JSONL file, one object per line. Same format as pi's sessions.

```jsonl
{"type":"session","version":3,"id":"01J...","cwd":"/home/user/project",...}
{"type":"message","id":"m1","parentId":null,"role":"user","content":"list .rs files","timestamp":"..."}
{"type":"model_change","id":"mc1","parentId":"m1","provider":"opencode_go","model_id":"deepseek-v4-pro","timestamp":"..."}
{"type":"message","id":"m2","parentId":"mc1","role":"assistant","content":"Found 3 files...","timestamp":"..."}
{"type":"message","id":"m3","parentId":"m2","role":"toolResult","toolCallId":"tool_01","content":"src/main.rs\n",...}
{"type":"compaction","id":"c1","parentId":"...","summary":"...","firstKeptEntryId":"...","tokensBefore":45000,...}
{"type":"branch_summary","id":"bs1","parentId":"...","fromId":"m10","summary":"Explored refactoring...",...}
```

### Entry types

```rust
pub enum SessionEntry {
    Message(MessageEntry),                          // conversation messages
    ThinkingLevelChange(ThinkingLevelChangeEntry),  // metadata
    ModelChange(ModelChangeEntry),                  // metadata
    ActiveToolsChange(ActiveToolsChangeEntry),      // metadata
    Compaction(CompactionEntry),                    // compaction summary
    BranchSummary(BranchSummaryEntry),              // branch navigation summary
    SessionInfo(SessionInfoEntry),                  // session name
    Label(LabelEntry),                              // tree node labels
    Custom(CustomEntry),                            // extension data
    CustomMessage(CustomMessageEntry),              // extension messages
    Leaf(LeafEntry),                                // tree leaf pointer
}
```

### Versioning

Current session version: **3**. Each entry has a unique `id` and optional
`parentId`, forming a tree. Branching writes a persistent `LeafEntry`
(pi-compatible durability — leaf position survives crashes).

---

## Session storage (`src/agent/session_storage.rs`) — ~630 lines

Pi-compatible `SessionStorage` trait with full CRUD operations.
Both implementations fully own their state (entries, by_id, labels, leaf_id).

### SessionStorage trait

```rust
pub trait SessionStorage: Send {
    fn metadata(&self) -> SessionMetadata;
    fn get_leaf_id(&self) -> Option<String>;
    fn set_leaf_id(&mut self, leaf_id: Option<&str>) -> Result<(), String>;
    fn create_entry_id(&self) -> String;
    fn append_entry(&mut self, entry: SessionEntry) -> Result<(), String>;
    fn get_entry(&self, id: &str) -> Option<SessionEntry>;
    fn find_entries(&self, type_name: &str) -> Vec<SessionEntry>;
    fn get_label(&self, id: &str) -> Option<String>;
    fn get_path_to_root(&self, leaf_id: Option<&str>) -> Result<Vec<SessionEntry>, String>;
    fn get_entries(&self) -> Vec<SessionEntry>;
    fn path(&self) -> Option<&Path>;
}
```

### Implementations

- **`JsonlSessionStorage`** — file-backed. Loads all entries into memory on
  open, persists on every `append_entry()` / `set_leaf_id()`. Leaf entries
  are written as `type: "leaf"` JSONL lines (pi-compatible).
- **`InMemorySessionStorage`** — fully in-memory. Used for `--no-session` mode
  and as initial storage before the first lazy flush.

### Leaf persistence

Branch navigation (`move_to` / `set_branch`) writes a persistent `LeafEntry`
with `type: "leaf"` and `targetId`, matching pi's durability model. The leaf
position survives crashes and is restored when the session file is reopened.

---

## Session repo (`src/agent/session_repo.rs`)

Higher-level session lifecycle management:

```rust
pub trait SessionRepo {
    fn list(&self, session_dir: &Path, filter_cwd: Option<&Path>,
            progress: Option<&dyn Fn(usize, usize)>) -> Vec<SessionInfo>;
    fn list_all(&self, progress: Option<&dyn Fn(usize, usize)>) -> Vec<SessionInfo>;
    fn delete(&self, path: &Path) -> io::Result<()>;
    fn fork(&self, source_path: &Path, target_dir: &Path,
            entry_id: Option<&str>, position: Option<&str>) -> io::Result<String>;
    fn load_info(&self, path: &Path) -> Option<SessionInfo>;
}
```

`DefaultSessionRepo` provides progress-aware, concurrent listing with
cwd filtering for the session picker UI.

---

## Compaction (`src/agent/compaction.rs`) — ~1140 lines ✅ IMPLEMENTED

When the conversation approaches the model's context window, older messages
are summarized to free space. Ported from pi's compaction algorithm.

### Algorithm

1. **Prepare** (`prepare_compaction()`) — Find the previous compaction
   boundary, scan entries, detect the cut point based on `keep_recent_tokens`.
   Handles split-turn detection (user message vs. mid-turn cut).

2. **Check threshold** (`should_compact()`) — If total tokens exceed
   `context_window - reserve_tokens`, compaction triggers.

3. **Summarize** (`compact()`) — Send older messages to the provider with a
   structured summarization prompt. Supports incremental updates (previous
   summary in `<previous-summary>` tags). Handles turn-prefix summarization
   for split turns.

4. **Replace** — Append a `CompactionEntry` to the session. The entry
   contains the summary, `first_kept_entry_id`, token count, and file
   operation details (readFiles/modifiedFiles).

### Settings

```rust
pub struct CompactionSettings {
    pub enabled: bool,
    pub reserve_tokens: u64,       // tokens reserved for system + response
    pub keep_recent_tokens: u64,   // newest tokens always kept
}
```

Defaults: enabled, 16K reserve, 20K keep recent. Configurable via settings
(`autoCompact`, `compactReserveTokens`, `compactKeepRecentTokens`).

### Manual trigger

Via `/compact` slash command → `AgentSession::run_manual_compact()` →
`compact()` → append `CompactionEntry`.

### Shared summarization helper

`summarize_text()` (shared with `branch_summary.rs`) calls yoagent's
`OpenAiCompatProvider` with a non-streaming text completion (no tools,
low temperature) to generate summaries.

---

## Branch summarization (`src/agent/branch_summary.rs`) — ~440 lines ✅ IMPLEMENTED

When the user navigates to a different branch in the session tree, the
abandoned branch is summarized so context is preserved.

### Algorithm

1. **Collect entries** (`collect_entries_for_branch_summary()`) — Walk from
   `old_leaf_id` back to the common ancestor with `target_id`. Produces
   chronological list of abandoned entries.

2. **Prepare messages** (`prepare_branch_entries()`) — Filter entries,
   respecting token budget. Tool results are skipped (context in assistant's
   tool calls). Compaction/branch summary entries are prioritized.

3. **Generate summary** (`generate_branch_summary()`) — Call the provider
   with a structured prompt (Goal / Progress / Key Decisions / Next Steps).
   Append a `BranchSummaryEntry` with file operation details (readFiles,
   modifiedFiles extracted from tool calls).

---

## Extension trait (`src/agent/extension.rs`)

All capability — built-in or user-provided — comes through the same trait.
Supports pi-compatible `beforeToolCall` / `afterToolCall` hooks, argument
type coercion via JSON Schema, and full schema validation.

```rust
#[async_trait]
pub trait Extension: Send + Sync {
    fn name(&self) -> Cow<'static, str>;

    /// Tools this extension provides (LLM-callable), each with prompt metadata.
    fn tools(&self) -> Vec<ToolDefinition> { vec![] }

    /// Slash commands (e.g. `/quit`, `/model`).
    fn commands(&self) -> Vec<SlashCommand> { vec![] }

    /// Skills this extension provides.
    fn skills(&self) -> yoagent::skills::SkillSet { yoagent::skills::SkillSet::empty() }
}
```

### Supporting types

```rust
/// A tool bundled with its prompt metadata.
pub struct ToolDefinition {
    pub tool: Box<dyn yoagent::types::AgentTool>,
    pub snippet: &'static str,
    pub guidelines: &'static [&'static str],
    /// Optional pre-processing of raw LLM arguments (type coercion).
    pub prepare_arguments: Option<fn(serde_json::Value) -> Result<serde_json::Value, String>>,
    /// Called before tool execution (pi's beforeToolCall).
    pub before_tool_call: Option<fn(&serde_json::Value) -> Option<BeforeToolCallResult>>,
    /// Called after tool execution (pi's afterToolCall).
    pub after_tool_call: Option<fn(&yoagent::types::ToolResult, bool) -> Option<AfterToolCallResult>>,
}

pub struct BeforeToolCallResult { pub block: bool, pub reason: String }
pub struct AfterToolCallResult { pub content: Option<Vec<Content>>, pub details: Option<Value>, pub is_error: Option<bool> }

pub struct AutocompleteItem { pub value: String, pub label: String, pub description: Option<String> }

pub trait CommandHandler: Send + Sync {
    fn execute(&self, args: &str) -> anyhow::Result<CommandResult>;
    fn argument_completions(&self, prefix: &str) -> Vec<AutocompleteItem>;
}

pub enum CommandResult { Info(String), Quit, ModelChanged(String), ShowHelp, Reloaded,
    NewSession, SessionSwitched, SessionInfo, OpenSessionSelector, SessionNamed,
    OpenSettings, ScopedModels, ExportSession, ImportSession, ShareSession,
    CopyLastMessage, ShowChangelog, ForkSession, CloneSession, SessionTree,
    TrustDecision, Login, Logout, CompactSession, ... }

pub struct SlashCommand { pub name: String, pub description: String, pub handler: Box<dyn CommandHandler> }

/// Tool-specific rendering interface.
pub trait ToolRenderer: Send + Sync {
    fn render_call(&self, args: &Value, width: usize, theme: &dyn Theme, ctx: &ToolRenderContext) -> Vec<String>;
    fn render_result(&self, content: &str, width: usize, theme: &dyn Theme, ctx: &ToolRenderContext) -> Vec<String>;
    fn render_self(&self) -> bool;
    fn render_bg_key(&self) -> Option<&'static str>;
}

/// Context passed to ToolRenderer methods (pi-compatible).
pub struct ToolRenderContext {
    pub expanded: bool,
    pub args_complete: bool,
    pub is_partial: bool,
    pub is_error: bool,
    pub cwd: String,
    pub duration_secs: Option<f64>,
    pub exit_code: Option<i32>,
    pub cancelled: bool,
    pub was_truncated: bool,
    pub full_output_path: Option<String>,
    pub file_path: Option<String>,
    pub expand_key: String,
    pub details: Option<serde_json::Value>,       // structured rendering data
    pub invalidate: Option<UnboundedSender<()>>,  // re-render request callback
}
```

### Argument coercion and validation

Every tool call goes through `ToolDefinition::execute()`:

1. **`prepare_arguments`** — custom per-tool pre-processing (e.g., write tool
   coerces `path`/`content` to strings)
2. **`coerce_with_json_schema()`** — recursive type coercion for common LLM
   mistakes (numbers as strings, booleans as strings, null handling, etc.)
3. **`validate_tool_arguments()`** — full JSON Schema validation with
   pi-compatible error paths (Required, additionalProperties, type mismatch)
4. **`before_tool_call`** — optional hook that can block execution
5. **Execute** — call inner `AgentTool::execute()`
6. **`after_tool_call`** — optional hook that can modify the result

At startup, extensions are collected from builtins + file_search:

```rust
let extensions: Vec<Box<dyn Extension>> = vec![
    Box::new(CommandsExtension::new(available_models)),
    Box::new(ReadExtension::new(cwd)),
    Box::new(WriteExtension::new(cwd)),
    Box::new(EditExtension::new(cwd)),
    Box::new(BashExtension::new(cwd)),
    Box::new(FileSearchExtension::new(cwd)),  // grep, find, ls
];
```

Extension gating is done via `is_extension_active()` which checks
`settings.tools` (whitelist) and `settings.exclude_tools` (blacklist).
Core extensions (commands, read, write, edit, bash, mcp) are always active
when no whitelist is set. Grep/find/ls are opt-in and bundled as a single
`FileSearchExtension`, activated if any of `"grep"`, `"find"`, or `"ls"`
is whitelisted in `settings.tools`.

---

## Built-in extensions (`builtin/`)

### commands — ✅ 22 slash commands

| Command | Result | Description |
|---------|--------|-------------|
| `/quit` | `Quit` | Graceful shutdown |
| `/model <name>` | `ModelChanged(name)` / `Info` | Switch model; no args lists available |
| `/settings` | `OpenSettings` | Open settings menu overlay |
| `/scoped-models` | `ScopedModels` | Enable/disable models for Ctrl+P cycling |
| `/export [path]` | `ExportSession { path }` | Export session (HTML or .jsonl) |
| `/import <path>` | `ImportSession { path }` | Import and resume a session |
| `/share` | `ShareSession` | Share as secret GitHub gist |
| `/copy` | `CopyLastMessage` | Copy last assistant message to clipboard |
| `/name <name>` | `SessionNamed { name }` | Set session display name |
| `/session` | `SessionInfo { ... }` | Show session info and stats |
| `/changelog` | `ShowChangelog` | Show changelog entries |
| `/hotkeys` | `ShowHelp` | Show keyboard shortcuts |
| `/fork [msg-id]` | `ForkSession { message_id }` | Fork from a previous message |
| `/clone` | `CloneSession` | Duplicate the current session |
| `/tree` | `SessionTree` | Navigate session tree |
| `/trust <decision>` | `TrustDecision { decision }` | Save project trust decision |
| `/login [provider]` | `Login { provider }` | Configure provider auth |
| `/logout [provider]` | `Logout { provider }` | Remove provider auth |
| `/new` | `NewSession` | Clear conversation |
| `/compact` | `CompactSession` | Manually compact session context |
| `/resume` | `OpenSessionSelector` | Open session selector |
| `/reload` | `Reloaded` | Reload keybindings, extensions, skills, prompts, themes |

### Built-in tools

| Tool | Key features |
|------|-------------|
| **read** | Path resolution, line numbers, 50KB truncation. Image support (base64 data URL). Pluggable `ReadOperations` trait. |
| **write** | Temp file + atomic rename, parent dir creation. `prepare_write_args` for type coercion. Preview rendering with syntax highlighting (partial+full). |
| **edit** | Exact-match search/replace, error on zero/multiple matches. Diff rendering with intra-line character-level inverse. Pluggable `EditOperations` trait. File mutation queue for serialized concurrent edits. |
| **bash** | `sh -c <command>`, configurable timeout, streaming via ToolProgress. Last 2000 lines / 50KB truncation. Pluggable `BashOperations` trait, command prefix, custom shell path. |

### File search extension (`src/extensions/file_search.rs`)

| Tool | Key features |
|------|-------------|
| **grep** | Uses ripgrep (`rg`) with `--json` output, falls back to `grep`. Respects .gitignore. Options: pattern, path, glob, ignoreCase, literal, context, limit (default 100). Pluggable `GrepOperations`. Shared `run_shell_command()` helper. |
| **find** | Uses `fd` (rust rewrite of find) with glob matching and .gitignore awareness, falls back to `find -name`. Options: pattern, path, limit (default 1000). Pluggable `FindOperations`. Shared `run_shell_command()` helper. |
| **ls** | Directory listing with `/` suffix for directories, dotfiles included. Options: path, limit (default 500). Pluggable `LsOperations`. Pure Rust via `std::fs::read_dir`. |

### File mutation queue (`src/builtin/file_mutation_queue.rs`)

Serializes concurrent file mutations targeting the same file path.
Different files run in parallel. Uses `tokio::sync::Notify` stored in a
global `LazyLock<Mutex<HashMap>>`. Operations chain through `Notify`
signals (each operation waits for the previous one's signal, then stores
its own for the next). Cleanup removes stale entries.

```rust
pub async fn with_file_mutation_queue<T, E, F, Fut>(
    file_path: &str,
    cwd: &Path,
    f: F,
) -> Result<T, E>
```

---

## Auth (`src/auth.rs`)

API key and OAuth credential storage, loaded from `~/.rab/agent/auth.json`.

```rust
pub enum AuthCredential {
    ApiKey { key: String },
    Oauth { access: String, refresh: Option<String>, expires: Option<i64>, enterprise_url: Option<String> },
}

pub struct AuthStorage(HashMap<String, AuthCredential>);
```

Load order: `~/.rab/agent/auth.json`. Supports multiple providers (api_key
or oauth). Methods: `api_key(provider)`, `oauth_token(provider)`.

---

## Footer data provider (`src/agent/footer_data_provider.rs`)

Provides git branch, extension statuses, and provider count to the Footer
on a **pull** basis. Owned by the App behind `Rc<RefCell<>>`.

Git branch resolution:
1. Walk up from `cwd` looking for `.git`
2. If `.git` is a file → worktree: parse `gitdir:` path, find HEAD
3. If `.git` is a directory → regular repo: find HEAD
4. Read HEAD file; if `ref: refs/heads/.invalid` → fall back to git
5. Otherwise treat as detached HEAD

Reftable repos are detected via the `.invalid` sentinel and fall back to
`git symbolic-ref --short HEAD`.

```rust
pub struct FooterDataProvider {
    cwd: PathBuf,
    git_branch: Option<String>,
    extension_statuses: BTreeMap<String, String>,
    available_provider_count: usize,
}
```

---

## Settings (`src/agent/settings.rs`) — ~800 lines

Same file names and format as pi, under `~/.rab/agent/`.

### Config files

| Pi path | rab path | Status |
|---|---|---|
| `~/.pi/agent/settings.json` | `~/.rab/agent/settings.json` ||
| `.pi/settings.json` | `.rab/settings.json` | ✅ (project-local overrides) |
| `~/.pi/agent/auth.json` | `~/.rab/agent/auth.json` ||
| `~/.pi/agent/models.json` | `~/.rab/models.json` | ⬜ Not implemented |
| `~/.pi/agent/AGENTS.md` | `~/.rab/agent/AGENTS.md` ||
| `AGENTS.md` / `CLAUDE.md` | `AGENTS.md` / `CLAUDE.md` | ✅ (project + ancestor walk) |
| `~/.pi/agent/keybindings.json` | `~/.rab/keybindings.json` ||
| `~/.pi/agent/sessions/` | `~/.rab/sessions/` ||
| `~/.pi/agent/skills/` | `~/.rab/skills/` ||
| `~/.pi/agent/themes/` | `~/.rab/themes/` + embedded ||

### `settings.json` format

```json
{
    "defaultModel": "deepseek-v4-flash",
    "defaultThinkingLevel": "high",
    "defaultProvider": "opencode_go",
    "tools": ["read", "write", "edit", "bash"],
    "excludeTools": [],
    "theme": "dark",
    "verbose": false,
    "hideThinkingBlock": true,
    "collapseToolOutput": true,
    "autoCompact": true,
    "compactReserveTokens": 16384,
    "compactKeepRecentTokens": 20000
}
```

Load order: global `~/.rab/agent/settings.json`, then project `.rab/settings.json`
overlays. CLI flags (`--model`, `--thinking`, `--no-context-files`, etc.) take
precedence over both. Settings modifications during a session (Ctrl+T, Ctrl+O,
Ctrl+Shift+C toggles) are tracked via `modified_fields` and only written
back for fields that were explicitly changed (preserving project-level overrides).

---

## System prompt (`src/agent/system_prompt.rs`) — ~400 lines

Built via `SystemPromptBuilder`:

1. **Default prompt** — tool descriptions, response format, tool guidelines,
   available tools from `ToolDefinition.snippet` / `.guidelines`.
2. **Custom SYSTEM.md**`~/.rab/agent/SYSTEM.md` (global) or `.rab/SYSTEM.md`
   (project). Replaces the default prompt.
3. **APPEND_SYSTEM.md** — appended after all prompts.
4. **Context files** — AGENTS.md/CLAUDE.md walked from cwd to root,
   wrapped in `<project_instructions path="...">` tags.
5. **Skills** — available skills listed as `<available_skills>` XML.
6. **Date and cwd**`Current date: YYYY-MM-DD`, `Current working directory: /path`.

Skills from extensions are merged via `skill_set.merge(ext.skills())`.

---

## CLI (`main.rs`) — manual arg parsing

```
rab [MESSAGE]...

Session:
  -c, --continue             Continue most recent session in cwd
  -r, --resume               Open interactive session picker
  --session PATH             Open specific session file (path or partial ID)
  --session-id ID            Create/open session with explicit ID
  --fork PATH                Fork a session from another session file
  --export PATH              Export session and exit (⚠ not yet implemented)
  --no-session               Ephemeral, don't save
  -n, --name <name>          Set session name

Model:
  --model MODEL              Model name (default: from settings.json)

Context:
  -nc, --no-context-files    Skip AGENTS.md/CLAUDE.md loading
  --system-prompt <text>     Replace default system prompt
  --append-system-prompt <text>  Append to system prompt

Other:
  --session-dir <path>       Session storage directory override
```

Session resolution supports both paths and partial UUID prefixes.
`--fork` resolves the session file, validates no conflicts with `--session-id`,
and calls `AgentSession::fork_from()`.

**Currently only OpenCode Go is configured** (via `yoagent::provider::OpenAiCompatProvider`
with `openai_compat()` model config pointing to `opencode.ai/zen/go/v1`).
Models: `deepseek-v4-flash` (default), `deepseek-v4-pro`.

Extension gating: `is_extension_active()` checks `settings.tools` whitelist
and `settings.exclude_tools` blacklist. Core extensions (commands, read,
write, edit, bash, mcp) are always active when no whitelist is set. File
search tools (grep, find, ls) are opt-in via the bundled `FileSearchExtension`.

---

## Run modes

### Print mode

```
$ rab "What does git status do?"
Shows the current state of the working directory and staging area...
```

```
$ cat README.md | rab "Summarize this"
```

Streams the response to stdout. Thinking blocks shown dimmed on stderr.
Tool calls and results shown prefixed with colored indicators. Has a 120s
timeout to prevent hanging on stuck providers. Uses a simple event loop:
`yoagent::agent::Agent::prompt_with_sender()` → process `AgentEvent` stream →
`agent_session.handle_yo_event()` for persistence.

### Interactive (TUI) mode

Same agent loop, different sink: `App` in `src/agent/ui/` subscribes to the
agent event stream and renders to a pi-tui-style main-screen TUI instead of
stdout. Uses `yoagent::agent::Agent::prompt_with_sender()` with event channels.

---

## TUI (`src/tui/` + `src/agent/ui/`) — 50+ modules, ~630 tests

The TUI library is a direct Rust port of pi's `@earendil-works/pi-tui`.

### Core (`src/tui/`)

| Module | Description |
|--------|-------------|
| `tui_core.rs` | `TUI` struct — event loop, render loop, overlay system, screen diff |
| `component.rs` | `Component` trait, `Size`, `RenderContext` |
| `container.rs` | Container layout — vertical, centered, flex-grow, children |
| `focusable.rs` | Focus management — `Focusable` trait, focus ring |
| `screen.rs` | Screen diff renderer — line-by-line comparison with cursor markers |
| `overlay.rs` | Overlay system — show/hide/composite, anchor-based positioning |
| `terminal.rs` | Terminal abstraction — `TerminalTrait`, `ProcessTerminal`, raw mode, cursor hide/show, synchronized output, `TerminalEvent::Key`/`Paste`/`Resize` |
| `keys.rs` | Key event handling — `key_event_to_id()`, 30+ action IDs, kitty protocol key encoding |
| `keybindings.rs` | JSON keybinding loading from `~/.rab/keybindings.json`, merge, resolution, 50+ action constants |
| `theme.rs` | Theme trait + default JSON theme loader |
| `fuzzy.rs` | Fuzzy matching for autocomplete |
| `autocomplete.rs` | Editor autocomplete popup — completions, rendering, keyboard navigation |
| `kill_ring.rs` | Kill ring for editor cut/copy/paste |
| `undo_stack.rs` | Undo/redo for editor |
| `word_nav.rs` | Word-boundary navigation utilities |
| `visual_truncate.rs` | Shared `truncate_to_visual_lines()` utility |
| `util.rs` | Shared utilities |

### Components (`src/tui/components/`)

| Module | Description |
|--------|-------------|
| `editor.rs` | Multi-line editor — word-wrap, undo stack, kill ring, paste markers, bracketed paste, history recall, character jump, sticky column, border_color, autocomplete |
| `markdown.rs` | comrak-based renderer with syntax highlighting, tables, code blocks, Kitty hyperlinks |
| `diff.rs` | Unified diff with colored +/lines and intra-line character-level inverse |
| `box.rs` | `Box` component with render cache, borders, backgrounds |
| `text.rs` | `Text` / `TruncatedText` with RefCell cache |
| `spacer.rs` | Vertical spacer |
| `select_list.rs` | Two-column selectable list with prefix filter |
| `loader.rs` | Animated loader with timer, colors, abort support |
| `dynamic_lines.rs` | Dynamically-sized section component |
| `rc_ref_cell_component.rs` | `RcRefCellComponent` bridge for shared ownership components |

### App layer (`src/agent/ui/`)

| Module | Description |
|--------|-------------|
| `app.rs` | Main `App` struct — event handler, agent loop management, message queuing, compose_ui, bang commands (!/!!), skills expansion |
| `chat_editor.rs` | `ChatEditor` wrapper — input processing, slash command dispatch, /skill:name expansion. `InputAction` enum with `Handled`, `Escape`, `Clear`, `Exit`, `ThinkingCycle`, `ModelSelector`, `ModelCycleForward`, `ModelCycleBackward`, `ToggleThinking`, `ToolsExpand`, `EditorExternal`, `Help`, `Submit`, `FollowUp`, `Dequeue`, `CompactToggle` |
| `theme.rs` | `RabTheme` — JSON theme resolution, fallback, color detection, 130+ theme keys |
| `working.rs` | `WorkingIndicator` — timer-based working animation |
| `model_selector.rs` | `ModelSelector` — Ctrl+P model cycling with scoped-models support |
| `footer.rs` | `Footer` — cwd, git branch, token usage, model, auto-compact indicator |
| `help.rs` | Help overlay with keybinding display |
| `render_utils.rs` | Shared rendering utilities |
| `components/header.rs` | Header — "rab" logo, keybinding hints |
| `components/footer_component.rs` | Footer component |
| `components/editor_component.rs` | Editor component with border_color |
| `components/user_message.rs` | User message component (box + markdown) |
| `components/assistant_message.rs` | Streaming assistant message with thinking blocks |
| `components/tool_messages.rs` | Tool execution components (read, write, edit, bash, grep, find, ls) with dedicated renderers |
| `components/info_message.rs` | Info message component (dim text) |
| `components/session_picker.rs` | Session selector overlay |
| `components/mod.rs` | Component re-exports |

### Layout (component tree)

```
Terminal (no alternate screen):
TUI.root (Container):
  ├── HeaderComponent ("rab" logo, keybinding hints)
  ├── chat_container (RefContainer)
  │   ├── UserMessageComponent
  │   ├── ToolExecComponent (read/write/edit/bash/grep/find/ls)
  │   ├── RcRefCellComponent → AssistantMessageComponent
  │   ├── InfoMessageComponent
  │   └── Spacer(1) between each
  ├── pending_section (DynamicLines — streaming text/thinking)
  ├── status_section (DynamicLines — transient status)
  ├── queued_section (DynamicLines — ◷ queued messages)
  ├── working_section (DynamicLines — ⠋ Working...)
  ├── EditorComponent (border color: thinking level / bash mode)
  └── FooterComponent (cwd, git branch, token usage, model, auto-compact)
```

### Message queuing and follow-up

When the user submits a message while streaming, it is queued via yoagent's
native `follow_up()` method. The agent loop's outer loop picks it up when the
current turn finishes — no concurrent agent loops. Queued messages appear
between chat and editor. On `AgentEnd`, the next queued message is
auto-submitted.

- **Follow-up** (`app.message.followUp`, default Alt+Enter) — queues a message
  during streaming without aborting the current turn.
- **Dequeue** (`app.message.dequeue`, default Alt+Up) — restores the queued
  message back to the editor.
- **Interrupt** (`app.interrupt`, default Ctrl+C) — aborts streaming and
  restores any queued messages to the editor.

### Keybinding actions

Keybinding actions are defined as constants in `src/tui/keybindings.rs`:

| Action constant | ID string | Default binding |
|-----------------|-----------|----------------|
| `ACTION_APP_ESCAPE` | `app.escape` | Escape |
| `ACTION_APP_CLEAR` | `app.clear` | Ctrl+C |
| `ACTION_APP_INTERRUPT` | `app.interrupt` | Ctrl+C (when streaming) |
| `ACTION_APP_EXIT` | `app.exit` | Ctrl+D |
| `ACTION_APP_SUSPEND` | `app.suspend` | Ctrl+Z |
| `ACTION_APP_THINKING_CYCLE` | `app.thinking.cycle` | Shift+Tab |
| `ACTION_APP_MODEL_SELECTOR` | `app.model.select` | Ctrl+L |
| `ACTION_APP_MODEL_CYCLE_FORWARD` | `app.model.cycleForward` | Ctrl+P |
| `ACTION_APP_MODEL_CYCLE_BACKWARD` | `app.model.cycleBackward` | Shift+Ctrl+P |
| `ACTION_APP_TOGGLE_THINKING` | `app.thinking.toggle` | Ctrl+T |
| `ACTION_APP_TOOLS_EXPAND` | `app.tools.expand` | Ctrl+O |
| `ACTION_APP_EDITOR_EXTERNAL` | `app.editor.external` | Ctrl+E |
| `ACTION_APP_HELP` | `app.help` | Ctrl+H |
| `ACTION_APP_HISTORY_UP` | `app.historyUp` | Ctrl+Up |
| `ACTION_APP_HISTORY_DOWN` | `app.historyDown` | Ctrl+Down |
| `ACTION_APP_MESSAGE_FOLLOW_UP` | `app.message.followUp` | Alt+Enter |
| `ACTION_APP_MESSAGE_DEQUEUE` | `app.message.dequeue` | Alt+Up |
| `ACTION_APP_COMPACT_TOGGLE` | `app.compact.toggle` | Shift+Ctrl+C |
| `ACTION_APP_SESSION_NEW` | `app.session.new` | Ctrl+N |
| `ACTION_APP_SESSION_TREE` | `app.session.tree` | Ctrl+G |
| `ACTION_APP_SESSION_FORK` | `app.session.fork` | Ctrl+F |
| `ACTION_APP_SESSION_RESUME` | `app.session.resume` | Ctrl+R |
| `ACTION_INPUT_SUBMIT` | `tui.input.submit` | Enter |
| `ACTION_INPUT_TAB` | `tui.input.tab` | Tab |
| `ACTION_INPUT_NEW_LINE` | `tui.input.newLine` | Alt+Enter (when idle) |
| `ACTION_INPUT_COPY` | `tui.input.copy` | Ctrl+Shift+C |

All bindings are customizable via `~/.rab/keybindings.json`.

### Transient status

Toggle messages (Ctrl+T thinking, Ctrl+O tool output, model switch, interrupt)
use `status_section` that appears for one frame then clears.

### Bang commands

- `!command` — run command, persist output to session as tool result
- `!!command` — run command, don't persist output, useful for viewing help/man

---

## Storage layout (`~/.rab/`)

```
~/.rab/
├── agent/
│   ├── settings.json          # global settings
│   ├── auth.json              # API keys and OAuth credentials
│   ├── SYSTEM.md              # custom system prompt (full override)
│   ├── APPEND_SYSTEM.md       # appended to system prompt
│   └── AGENTS.md              # global context file
├── models.json                # ⬜ custom provider/model definitions (not implemented)
├── keybindings.json           # custom keybindings
├── extensions/                # ⬜ WASM plugins (Phase 2)
├── skills/                    # agent skills (SKILL.md files)
├── themes/                    # TUI themes
└── sessions/
    └── <cwd-hash>/            # one directory per project
        ├── 01J...abc.jsonl
        └── 01J...def.jsonl

./
├── .rab/
│   ├── settings.json          # project-local overrides
│   ├── SYSTEM.md              # project-local system prompt
│   └── APPEND_SYSTEM.md       # project-local append prompt
├── AGENTS.md                  # project context (also walks parent dirs)
└── CLAUDE.md                  # alias for AGENTS.md
```

---

## Dependency tree

```
rab (EPL-2.0)
├── yoagent 0.8.4         (MIT)        - agent loop, provider, types, skills
│   └── reqwest           (Apache 2.0) - HTTP client (inside yoagent)
├── tokio 1               (MIT)        - async runtime
├── tokio-util 0.7        (MIT)        - CancellationToken
├── serde + serde_json 1  (MIT)        - JSON serialization
├── uuid 1                (MIT)        - message/session IDs
├── chrono 0.4            (MIT)        - timestamps
├── directories 6         (MIT)        - XDG paths
├── anyhow 1              (MIT)        - error handling
├── futures 0.3           (MIT)        - StreamExt
├── async-trait 0.1       (MIT)        - trait async fn
├── colored 3             (MPL-2.0)    - terminal colors
├── crossterm 0.29        (MIT)        - terminal I/O
├── unicode-segmentation 1 (MIT)       - grapheme-aware cursor movement
├── unicode-width 0.2     (MIT)        - character display width
├── unicode-normalization 0.1 (MIT)    - Unicode normalization
├── comrak 0.52           (MIT)        - markdown parsing (GFM)
├── syntect 5.3           (MIT)        - syntax highlighting (optional feature)
├── base64 0.22           (MIT)        - image data URL encoding
├── async-stream 0.3      (MIT)        - async stream generation
├── regex 1.12            (MIT)        - regex
├── reqwest 0.12          (MIT/Apache 2.0) - HTTP client (rustls-tls, socks)
├── openssl-sys 0.9       (MIT)        - vendored OpenSSL
├── libc 0.2              (MIT)        - system calls
# wasmtime 26+ (Phase 2, Apache 2.0)
# notify 7    (Phase 2, CC0-1.0)
# rmcp 1      (Phase 2, MIT)
```

No GPL dependencies. All permissive (MIT / Apache 2.0 / MPL-2.0), fully
compatible with EPL-2.0. Phase 2 dependencies (wasmtime, notify, rmcp) are
gated behind Cargo features: `plugins` and `mcp`. MVP compiles without them.

---

## Phase 2

### WASM plugin system — ⬜ NOT STARTED

Planned: WASM components via wasmtime, loaded from `~/.rab/extensions/`.
Same `Extension` trait used by builtins — plugins implement it via WIT
bindings. Hot reload via file watcher.

See the old architecture doc for the detailed WIT interface and
plugin author experience (design deferred to Phase 2).

### MCP adapter — ✅ IMPLEMENTED

The `src/extensions/mcp/` module (6 modules, ~2040 lines) provides a full MCP
adapter matching pi-mcp-adapter's architecture. Configured via `~/.rab/agent/mcp.json`
(global) and `.rab/mcp.json` (project-local overrides).

#### Key components:

| Module | Description |
|--------|-------------|
| `mcp/mod.rs` | `McpExtension` struct, proxy `mcp` tool, `McpDirectTool` adapter |
| `mcp/config.rs` | Config loading + merging (global + project), server config hashing |
| `mcp/server.rs` | `ServerManager` — lazy connection, idle timeout, keep-alive, SSE-aware HTTP transport |
| `mcp/types.rs` | `McpConfig`, `ServerEntry`, `McpSettings`, `MetadataCache`, tool name formatting |
| `mcp/cache.rs` | Persistent metadata cache (`mcp-cache.json`) for fast startup |
| `mcp/renderer.rs` | `McpToolRenderer` and `McpProxyToolRenderer` for TUI rendering |

#### Dual architecture:
- **Proxy tool** (`mcp` gateway) — status, list, search, describe, connect, call, auth actions
- **Direct tools** — servers with `directTools` enabled register individual tools as native `AgentTool`s

#### Server lifecycle:
- `lazy` (default) — connects on first use, disconnects after idle timeout
- `eager` — connects at agent startup
- `keep-alive` — never disconnects

### models.json — ⬜ NOT IMPLEMENTED

Custom provider/model definitions. Currently OpenCode Go is the only
configured backend.

### User extensions (compile-time)

Already possible today — implement the `Extension` trait and register in
`main.rs`. No dynamic loading yet.

---

## Open questions

- **Image paste in TUI** — clipboard integration differs per platform
  (wl-paste, pbpaste, PowerShell). Kitty protocol covers display; input TBD.
- **Command deny-list** — bash tool currently runs anything. A deny-list or
  sandbox (bubblewrap, landlock) should be configurable.
- **Provider fallback** — if the primary provider fails, should rab retry
  with another? yoagent handles basic retry; full fallback chain TBD.
- **Multi-model cycling** — Ctrl+P model switching uses a static list.
  A full model registry with metadata (context window, costs) is future work.