recursive-agent 0.6.0

A minimal, orthogonal, self-improving coding agent kernel in Rust
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
# Recursive TUI — 人工验收清单 (Acceptance Checklist)

> 生成方式:从 `src/tui/` 下 25 个 Rust 源文件**逐行抽出的实际行为**,不是设计文档,不是理想状态。
>
> 用途:把"代码现在这样"摆出来,让你**逐个功能判断**是否符合你的预期。不符合的 → 改代码 → 改完更新本文档 → 等清单基本全绿了再写单测锁住。
>
> 维护:`docs/tui-fake-cc-gap.md` 是相对 fake-cc 的对账单;本文档是相对"你想要的产品"的验收单。两份互补,别混淆。

## 图例

- [ ] = **未验收**:你需要手动跑一遍判断
- [x] = **符合预期**
- [~] = **部分符合 / 有 bug / 需要再设计**
- [N/A] = **本场景不打算支持**(如真不支持,请在备注里写理由)

每条尽量填这三栏:**期望行为(代码现状)** / **怎么验证** / **状态**。

---

## A. 启动 / 屏幕

### A.1 Splash 屏
- 期望:启动后 0-2s 显示 splash,2s 后自动切到 Chat 屏;或任意按键(Press key event)立即切到 Chat
- 验证:`cargo run` → 等 2s,看是否切到 Chat;或者 splash 期间按任意键,看是否立即切
- 来源:`src/tui/app.rs:160` (`screen: AppScreen::Splash` 默认), `src/tui/mod.rs:78` (2s timer), `src/tui/mod.rs:57` (任意 Key 切)
- 状态:[ ]

### A.2 启动后立刻显示的欢迎块
- 期望:transcript 第一块是 `System: "Welcome to Recursive TUI. Type a message and press Enter."`
- 验证:启动后看 transcript 第一行
- 来源:`src/tui/app.rs:153-155`
- 状态:[ ]

### A.3 屏幕只有两种状态
- 期望:`AppScreen` 只有 `Splash``Chat`,没有别的(如无 Onboarding、Doctor、Resume 屏)
- 验证:grep `AppScreen::` 只能找到这两个
- 来源:`src/tui/model.rs:18-21`
- 状态:[ ]

---

## B. 输入模式 (InputMode)

> 输入框支持 6 种模式,自动从首字符识别,可 Shift+Tab 循环。

### B.1 模式自动识别
- 期望:缓冲为空时按 `!` → 进入 Bash 模式(不存 `!`);按 `#` → 进入 Note(不存 `#`);按 `/` → 进入 Command(不存 `/`);其它首字符照常插入
- 验证:清空输入框,分别按 `!` `echo hi` Enter、`#` `my note` Enter、`/` `help` Enter 看行为
- 来源:`src/tui/app.rs:522-552` (`handle_char_input`)
- 状态:[ ]

### B.2 Shift+Tab 循环
- 期望:Prompt → Bash → Note → Prompt;Command / AtFile / HistorySearch 不会出现在循环里
- 验证:在 Prompt 按 Shift+Tab 几次,看 indicator 是不是按 ❯ → ! → # → ❯ 切
- 来源:`src/tui/input_state.rs:104-111` (`cycle_next`)
- 状态:[ ]

### B.3 Backspace 退模式
- 期望:在 Bash/Note/Command 模式下,缓冲为空时按 Backspace → 退回 Prompt 模式(不退字符)
- 验证:进入 Bash 模式不输字符,按 Backspace,看 indicator 是不是变回 ``
- 来源:`src/tui/app.rs:380-388`
- 状态:[ ]

### B.4 Prompt 模式提交
- 期望:在 Prompt 模式下按 Enter → 推送 `User { text: "..." }` 块到 transcript,scroll 到底,turn_count++,发出 `UserAction::SendMessage`
- 验证:输 "hello" Enter,看 transcript 是否多一个 `▎ You` 块,状态栏 turn 数 +1
- 来源:`src/tui/app.rs:557-617` (`submit_prompt`)
- 状态:[ ]

### B.5 Bash 模式提交
- 期望:在 Bash 模式下按 Enter → 推 `User { text: "!..." }` 块,发出 `UserAction::RunShell`(不经 LLM)
- 验证:进入 Bash 模式输 `echo hi` Enter,看 transcript 是否多 `!echo hi` 块,然后是否出现 `🔧 run_shell ...` + `✓ run_shell ...`
- 来源:`src/tui/app.rs:576-582`, `src/tui/bash.rs::run_bash_command`
- 状态:[ ]

### B.6 Note 模式提交
- 期望:在 Note 模式下按 Enter → 推 `System { text: "# ..." }` 块;**不发** UserAction(不调 LLM、不调 backend)
- 验证:进入 Note 模式输 `my note` Enter,看 transcript 是否多一行 `# my note`(灰色 italic),且 backend 不消耗 token
- 来源:`src/tui/app.rs:583-589`
- 状态:[ ]

### B.7 Command 模式提交
- 期望:在 Command 模式下按 Enter → 走 `dispatch_slash_command` 分发到内置命令或 skill
- 验证:`/help` Enter 弹出 Help modal;`/foo` Enter 弹 Error 块 "Unknown command"
- 来源:`src/tui/app.rs:590, 622-673`
- 状态:[ ]

### B.8 多行输入
- 期望:Shift+Enter(或 Alt+Enter fallback)插入 `\n` 而不是提交;输入框高度自适应,最多 6 行(超过 6 行内滚)
- 验证:Shift+Enter 看是否换行;输入很多行看输入框高度是否封顶
- 来源:`src/tui/app.rs:331-338`, `src/tui/ui/input.rs:24, 32-40`
- 状态:[ ]

### B.9 行内编辑
- 期望:←/→ 按字符移动光标(UTF-8 安全);Home/End 移到本行首/尾;Ctrl+A 行首;Ctrl+E 在非空 buffer 下 = End,在空 buffer 下 = 切换最近 ToolResult 的展开
- 验证:输入 "abc" 在 c 后面按 Home 看光标到 a;输入 "你好" 在第二个字后面按 ← 看光标是否跨过整个汉字(不是 1 列)
- 来源:`src/tui/input_state.rs:200-249`, `src/tui/app.rs:249-256` (Ctrl+E)
- 状态:[ ]

### B.10 Backspace/Delete
- 期望:Backspace 删左边一个 UTF-8 char;Delete 删右边一个 char;光标位置是 byte offset 永远在 char boundary
- 验证:输入 "你好" Backspace 看是否一次删一个字(不是 3 字节)
- 来源:`src/tui/input_state.rs:171-198`
- 状态:[ ]

### B.11 历史回溯
- 期望:缓冲为空时按 ↑ 走历史(older),按 ↓ 走历史(newer),走过最新一条时恢复当前 draft;上行/下行有 200 条 cap
- 验证:连续提交 3 条消息,缓冲空时按 ↑ 看是否回显之前消息,draft 是否保留
- 来源:`src/tui/input_state.rs:253-322` (`history_prev` / `history_next`), `HISTORY_CAPACITY = 200`
- 状态:[ ]

### B.12 模式前缀保留
- 期望:历史回溯时自动解析前缀(`!`/`#`/`/`);恢复时切回对应 mode
- 验证:提交 `!ls`、提交 `#note`、提交 `/help`,然后 ↑ 走,看 indicator 是不是分别变 `!` `#` `/`
- 来源:`src/tui/input_state.rs:298-304, 325-335` (`strip_history_prefix`)
- 状态:[ ]

### B.13 Ctrl+B / Ctrl+F 滚动
- 期望:Ctrl+B scroll down 10 行;Ctrl+F scroll up 10 行(pgup/pgdn 不可靠的 macOS 终端的备选)
- 验证:长 transcript(>10 屏)下按 Ctrl+B,看 transcript 是否向上滚
- 来源:`src/tui/app.rs:271-278`
- 状态:[ ]

### B.14 Shift+↑/↓ 滚动
- 期望:Shift+↑ 向下滚动 1 行(scroll down 1 行,看更老的内容),Shift+↓ 向上滚动 1 行
- 验证:长 transcript 滚到底后按 Shift+↑ 看是否能看到更早内容
- 来源:`src/tui/app.rs:347-354`
- 状态:[ ]

### B.15 'q' 在空 buffer 退出
- 期望:输入框为空时按 'q' → `should_quit = true`;输入框非空时 'q' 是普通字符
- 验证:清空 buffer 按 q 看 TUI 是否退出;输入 "qqq" 看 q 是否被插入
- 来源:`src/tui/app.rs:371-374`
- 状态:[ ]

### B.16 Ctrl+D
- 期望:未实现(代码里没看到对 Ctrl+D 的专门处理),行为未定义
- 验证:按 Ctrl+D 看会怎样(crossterm 终端默认可能输出 EOF,与代码行为无关)
- 备注:`docs/tui-fake-cc-gap.md` 列了 fake-cc 是退出,但本项目代码未处理
- 状态:[ ]

### B.17 Esc 行为(无 modal 时)
- 期望:buffer 非空 → 清空 + 回到 Prompt;mode 非 Prompt → 回到 Prompt;turn 在跑 → 发 `UserAction::Interrupt` + 推 "Interrupting…" System 块;其它 → **no-op,不退出**(Goal 147 显式不退出)
- 验证:分别测这 4 种场景
- 来源:`src/tui/app.rs:428-454` (`handle_esc`)
- 状态:[ ]

### B.18 Ctrl+C 行为(无 modal 时)
- 期望:第一次按 → 优先级:modal 弹栈 > buffer 清空 > 中断 turn > 提示 "Press Ctrl+C again to exit";**2 秒内**第二次按 → 真退出(`should_quit = true`- 验证:在 chat 屏按一次 Ctrl+C 看 hint,再按一次看退出
- 来源:`src/tui/app.rs:464-505` (`handle_ctrl_c`), `DOUBLE_PRESS_WINDOW = 2000ms`
- 状态:[ ]

### B.19 双击窗口可调
- 期望:`RECURSIVE_TUI_DOUBLE_MS` 环境变量可改双击窗口(默认 2000ms)
- 验证:`RECURSIVE_TUI_DOUBLE_MS=500 cargo run` 测 0.5s 内连按
- 来源:`src/tui/input_state.rs:16, 22-28`
- 状态:[ ]

---

## C. Slash 命令 (15 个内置 + skill)

> 调命令必须先在输入框输 `/`,按 Enter。

### C.1 `/help` (alias `/?`)
- 期望:弹出 Help modal,列出所有命令 + 键位说明
- 验证:输 `/help` Enter,看 modal
- 来源:`src/tui/commands.rs:284-286`, `src/tui/ui/modal.rs:183-255` (`render_help_body`)
- 状态:[ ]

### C.2 `/clear` (alias `/cls`)
- 期望:清空 transcript + 重置 UsageStats + 重置 turn_count = 0 + 推 "Conversation cleared." System 块
- 验证:先有几个 block 然后 `/clear`,看是否只剩 1 个 cleared 块
- 来源:`src/tui/commands.rs:288-291`, `src/tui/app.rs:990-999` (`reset_transcript`)
- 状态:[ ]

### C.3 `/compact`
- 期望:推 "Compacting transcript…" System 块,发 `UserAction::Compact`,等 runtime 推 `UiEvent::Compacted` → 推 `Compacted { removed, kept }`- 验证:`/compact` Enter,看 transcript 是否出现 compacted 块
- 来源:`src/tui/commands.rs:293-296`
- 状态:[ ]

### C.4 `/cost`
- 期望:弹出 CostDetail modal,显示 token 用量 + 估算成本(按模型)+ 最近延迟
- 验证:用一会后 `/cost`,看 modal
- 来源:`src/tui/commands.rs:298-300`
- 状态:[ ]

### C.5 `/model`
- 期望:弹出 ModelInfo modal(**只读**,不切换),显示当前模型 + provider 推断 + endpoint
- 验证:`/model`,看 modal
- 备注:注释明确说"switching models requires restart"
- 来源:`src/tui/commands.rs:302-304`, `src/tui/ui/modal.rs:322-355` (`render_model_body`)
- 状态:[ ]

### C.6 `/status`
- 期望:推一个 System 块:"Status — turn N, blocks M, tokens K, uptime Us, planning on/off"
- 验证:`/status`,看最新 System 块内容
- 来源:`src/tui/commands.rs:306-320`
- 状态:[ ]

### C.7 `/tools`
- 期望:弹出 ToolList modal,列出当前注册的工具(offline 时是默认 6 个)
- 验证:`/tools`,看 modal 里的工具列表
- 来源:`src/tui/commands.rs:322-326`
- 状态:[ ]

### C.8 `/plan on` / `/plan off`
- 期望:切换 `planning_mode_on` flag + 推 System 块确认 + 发 `UserAction::SetPlanningMode`;不带参数 → 推 "Usage: /plan on|off" Error 块
- 验证:`/plan on` 看状态栏是否出现 "plan-first" 段;`/plan`(无参)看 Error
- 来源:`src/tui/commands.rs:328-341`
- 状态:[ ]

### C.9 `/journal`
- 期望:弹 Journal modal,列出 `.dev/journal/` 最近 5 个 .md,每个前 30 行
- 验证:`/journal`,看 modal
- 来源:`src/tui/commands.rs:343-349`
- 状态:[ ]

### C.10 `/permissions on|off` (alias `/perm`)
- 期望:开/关 runtime permission hook;`/permissions off` 同时清空 `auto_allowed_tools`,并 deny 当前 pending permission;不带参数 → 提示 "Usage: ...  (currently on/off)"
- 验证:`/permissions on` + 在某次 tool call 时是否弹 permission modal;`/permissions` 无参看错误提示
- 来源:`src/tui/commands.rs:356-389`
- 状态:[ ]

### C.11 `/exit` (aliases `/quit`, `/q`)
- 期望:`should_quit = true`
- 验证:分别 `/exit` `/q` `/quit` 都应退出
- 来源:`src/tui/commands.rs:351-354`
- 状态:[ ]

### C.12 `/goal <cond> [or stop after N turns]`
- 期望:
  - 无参数 → 推 "Goal: ..." 状态(如果有)或 "No active goal."
  - `clear` → 清目标 + 发 `UserAction::ClearGoal`
  - 带条件 → 推 "Goal set: ..." + 发 `UserAction::SetGoal { condition, max_turns }`
- 验证:`/goal make tests pass``/goal make tests pass or stop after 5 turns``/goal``/goal clear`
- 来源:`src/tui/commands.rs:397-450`
- 状态:[ ]

### C.13 `/resume` (alias `/r`)
- 期望:弹 ResumePicker modal(最近 20 个 session),↑/↓ 选,Enter 加载(发 `UserAction::ResumeSession`),Esc/q 取消;无 session → 推 "No saved sessions found." Error
- 验证:`/resume`,看 modal;选一个看是否加载
- 来源:`src/tui/commands.rs:456-468`, `src/tui/ui/modal.rs:433-474`, `src/tui/app.rs:1403-1439`
- 状态:[ ]

### C.14 `/mcp`
- 期望:发 `UserAction::ListMcpServers`,等 runtime 推 `UiEvent::McpServersLoaded` → 弹 McpServers modal
- 验证:`/mcp`,看是否列 MCP server
- 来源:`src/tui/commands.rs:470-472`
- 状态:[ ]

### C.15 `/theme <name>` (default `dark`)
- 期望:
  - 无参数 → 推 "Current theme: dark. Available: dark, light, solarized"
  - `dark` / `light` / `solarized` → 切 + 推 "Theme switched to '...'"
  - 未知 → 推 Error "Unknown theme '...'"
- 验证:`/theme light` 看 status bar 颜色;`/theme` 看可用列表
- 来源:`src/tui/commands.rs:474-498`, `src/tui/ui/theme.rs::ALL_THEMES`
- 状态:[ ]

### C.16 未知命令
- 期望:推 "Unknown command: /xxx. Try /help." Error 块
- 验证:`/foobar` Enter
- 来源:`src/tui/app.rs:670-672`
- 状态:[ ]

### C.17 命令补全 (Tab / ↑↓ / Enter)
- 期望:Command 模式下输入 `/``/xxx` 时,命令菜单 popup 实时匹配(按名字 + alias 名字前缀);Tab 补全到共同前缀;↑↓ 在菜单里选;Enter 选中并 dispatch(或用 literal buffer)
- 验证:输 `/co` 看 popup,应有 `compact` `cost` 两条
- 来源:`src/tui/app.rs:324-328` (Command 模式路由), `src/tui/app.rs:1005-1060` (`handle_command_menu_key`), `src/tui/commands.rs:246-257` (`search`)
- 状态:[ ]

### C.18 Skill 命令
- 期望:从 `<workspace>/.recursive/skills/*.md` 加载,frontmatter 含 name/description/aliases/argument_hint/allowed_tools;正文中的 `$ARGUMENTS` 替换为用户参数;同名 skill 被内置命令 shadow
- 验证:写一个 `.recursive/skills/test.md`,内容是 "Echo: $ARGUMENTS",再 `/test hello` 看 transcript
- 来源:`src/tui/skill_commands.rs`
- 状态:[ ]

---

## D. 模态框 (9 种)

> 模态栈后入先出,最上面那个吃键。Esc / q 弹最上面一个(特例:PlanReview y/n/Esc 走专属逻辑,详见 K 节)。

### D.1 Help modal
- 期望:列所有内置命令(带 alias)、所有 skill 命令(如果有)、键位说明;Esc/q 关闭
- 来源:`src/tui/ui/modal.rs:69, 183-255`
- 状态:[ ]

### D.2 CostDetail modal
- 期望:标题 "Token usage";显示 input / output / total tokens + 估算成本(按模型定价表查;无定价显示 "(no pricing)")+ 最近延迟 + provider
- 来源:`src/tui/ui/modal.rs:69, 257-320`
- 状态:[ ]

### D.3 ModelInfo modal
- 期望:标题 "Model";显示模型名 / 推断的 provider(按模型名前缀 deepseek/glm/claude/gpt)/ endpoint(`RECURSIVE_API_BASE` 或默认 OpenAI)
- 验证:分别换 `RECURSIVE_API_BASE` 启动,看 endpoint 是否变
- 来源:`src/tui/ui/modal.rs:69, 322-355`
- 状态:[ ]

### D.4 ToolList modal
- 期望:标题 "Tools";列所有 `(name, desc)`;空列表显示 "(no tools registered)"
- 来源:`src/tui/ui/modal.rs:69, 357-383`
- 状态:[ ]

### D.5 Journal modal
- 期望:标题 "Journal";列最近 5 个 .md;当前选中的显示完整 preview(前 30 行);↑↓ 切换,Esc/q 关闭
- 来源:`src/tui/ui/modal.rs:69, 385-431`
- 状态:[ ]

### D.6 Confirm modal
- 期望:y/Enter → 执行 `on_yes` (Exit 或 Clear);n/Esc → 仅弹栈;非 y/n 键无反应
- 验证:观察 `/clear` 弹 Confirm 吗(**注意**`/clear` 当前是直接清,不弹 Confirm;只有未来可能加的场景用)
- 备注:当前代码里只在 modal.rs 定义了 type,没有实际 push Confirm 的地方(搜索 `Modal::Confirm` 只有定义和测试)—— 可能是预留
- 状态:[ ]

### D.7 PlanReview modal
- 期望:见 K.2 节
- 状态:[ ]

### D.8 ResumePicker modal
- 期望:见 C.13
- 状态:[ ]

### D.9 McpServers modal
- 期望:标题 "MCP Servers";列 `(name, transport, enabled)`;selected 行黄色加粗;↑↓ 切换,Esc/q 关闭;**没有启用/禁用 toggle**(只能看)
- 验证:`/mcp` 后看 modal
- 备注:`McpEntry.enabled` 字段存在但 UI 上没操作入口
- 来源:`src/tui/ui/modal.rs:69, 476-522`, `src/tui/app.rs:1442-1467`
- 状态:[ ]

### D.10 Modal 渲染共性
- 期望:占 70% × 70% 居中区域 + Clear 暗背景 + Cyan 边框 + 黑底;title 在顶部 border
- 来源:`src/tui/ui/modal.rs:136-168`
- 状态:[ ]

### D.11 Modal 暗背景
- 期望:模态占的整个矩形被 `Clear` 覆盖(不是只覆盖文本区)
- 验证:弹模态时,看模态外的部分是不是变黑
- 状态:[ ]

---

## E. Transcript 块 (10 种)

> 块间用一个空行分隔。

### E.1 User 块
- 期望:标题 "▎ You"(LightBlue bold);内容用 `` 缩进每行;空文本时也有 `` 占位
- 来源:`src/tui/ui/transcript.rs:62-86`
- 状态:[ ]

### E.2 Assistant 块(含流式)
- 期望:
  - 标题 "▎ Agent"(LightCyan bold)
  -`latency_ms` 时显示 "⏱ X.Xs"(Gray)
  - `streaming: true` 时显示 "…streaming"(Gray italic)
  - 内容:表格用 `render_table` 渲染;其它行用 `render_inline`(含 markdown inline 解析)
  - 空文本时也有 `` 占位
- 验证:观察 streaming 时的 "…streaming" 是否出现;terminal turn 完后 "…streaming" 是否消失
- 来源:`src/tui/ui/transcript.rs:90-153`
- 状态:[ ]

### E.3 ToolCall 块
- 期望:单行;`  🔧 <name>  <args_preview>`;name Yellow bold;preview Gray dim
- 验证:跑一个 `read_file` 看 ToolCall 块
- 来源:`src/tui/ui/transcript.rs:157-174`
- 状态:[ ]

### E.4 ToolResult 块
- 期望:
  - 标题 `  ✓/<name>  (<size>)``  ✗/<name>`(成功绿、失败红,name bold)
  - `output > 6 行``expanded: false`:前 3 行 + "... (N more lines, press Ctrl+E to expand)"
  - `expanded: true`:全显示
- 验证:跑一个长输出的 tool,看 3 行截断 + 提示;按 Ctrl+E 展开
- 来源:`src/tui/ui/transcript.rs:178-242`
- 状态:[ ]

### E.5 Diff 块
- 期望:
  - 有 hunks:顶部路径 + hunk body(按 +/- 染色;红减绿加)
  - 无 hunks(如 write_file 合成):"Updated path (N bytes)" 样式
- 验证:`apply_patch` 触发 Diff 块;`write_file` 触发 stub
- 来源:`src/tui/ui/diff.rs`, `src/tui/ui/transcript.rs:246-254`, `src/tui/app.rs:704-708, 723-731`
- 状态:[ ]

### E.6 Compacted 块
- 期望:单行 `  ⊕ Conversation compacted: <removed> messages → <kept> summary`(Gray italic)
- 来源:`src/tui/ui/transcript.rs:258-270`
- 状态:[ ]

### E.7 System 块
- 期望:单行;Gray italic
- 来源:`src/tui/ui/transcript.rs:272-279`
- 状态:[ ]

### E.8 Error 块
- 期望:单行;Red
- 验证:故意触发一个错误(如 LLM 不可达),看是否红色显示
- 来源:`src/tui/ui/transcript.rs:281-286`
- 状态:[ ]

### E.9 PlanProposal 块(inline,**不是 modal**
- 期望:见 K.2 节
- 状态:[ ]

### E.10 PlanModeRequest 块(inline)
- 期望:见 K.3 节
- 状态:[ ]

### E.11 块 append 顺序保证
- 期望:所有 UiEvent 处理后,如果当前 `scroll_offset == 0` 自动滚到底;非 0 时保留位置(不被流式输出 yank 回去)
- 验证:长 transcript 滚到中段后等流式输出,看是否保持位置
- 来源:`src/tui/app.rs:946-953`
- 状态:[ ]

---

## F. Status bar

> 底部单行(白字 + DarkGray 背景),segment 间 `` 分隔。

### F.1 段位固定
- 期望(按顺序):`local` (green bold) │ `<model>` (cyan) │ `↑<in> ↓<out>` (white) │ `$<cost>` (yellow, 可选) │ `turn <N>` (white) │ `plan: y/n``plan-first` (yellow, 条件) │ `⏱ Xs` (magenta, 仅运行中)
- 验证:观察 status bar
- 来源:`src/tui/ui/status.rs:32-111` (`build_line`)
- 状态:[ ]

### F.2 成本显示规则
- 期望:模型在 `default_pricing_table()` 里有定价才显示 `$X.XXXX`;没有则省略
- 验证:分别用 `gpt-4o-mini` (有定价) 和 `bogus-model` (无) 跑对比
- 来源:`src/tui/cost.rs:79-88`, `src/tui/ui/status.rs:62-73`
- 状态:[ ]

### F.3 令牌格式
- 期望:`<1k` 显示原数字;`1k-1M` 显示 `X.Yk``≥1M` 显示 `X.YM`
- 验证:跑足够多 turn 让 token 上 1k
- 来源:`src/tui/ui/status.rs:121-129` (`human_count`)
- 状态:[ ]

### F.4 计时
- 期望:turn 没跑不显示 ``;turn 在跑显示 `⏱ X.Xs`,每秒更新
- 验证:发起一个 turn 看 status bar
- 来源:`src/tui/ui/status.rs:101-108`
- 状态:[ ]

### F.5 Plan 段优先级
- 期望:`plan_awaiting_approval` 时显示 `plan: y/n`(黑底黄字,加粗);否则若 `planning_mode_on` 显示 `plan-first`;都没有则不显示
- 验证:`/plan on` 看 status bar 出现 `plan-first`;agent 进入 plan mode 后看 `plan: y/n`
- 来源:`src/tui/ui/status.rs:83-98`
- 状态:[ ]

### F.6 Model name 探测
- 期望:优先级 `RECURSIVE_MODEL` > `OPENAI_MODEL` > `~/.recursive/config.toml``[provider].model` > `gpt-4o-mini` 默认
- 验证:分别设这些环境变量 / 配置文件
- 来源:`src/tui/cost.rs:98-113` (`detect_model_name`)
- 状态:[ ]

---

## G. Bash 模式 (! 前缀)

> 不经 LLM,直接调 `run_shell` 工具。

### G.1 不入 runtime transcript
- 期望:`!echo hi` 输出 ToolCall/ToolResult 块到 TUI transcript,但****进入 runtime 的对话历史
- 验证:先 `!pwd` 看输出,然后正常对话,看 LLM 是否"记得"刚 `!pwd` 的内容(应该不记得)
- 来源:`src/tui/bash.rs::run_bash_command` (只 emit 事件, 不走 `g.run``g.enqueue`)
- 状态:[ ]

### G.2 超时
- 期望:`run_shell` 默认 300s 超时
- 来源:`src/tui/bash.rs:14` (`with_timeout(Duration::from_secs(300))`)
- 状态:[ ]

### G.3 错误显示
- 期望:命令失败时 ToolResult `success: false`,输出前缀 `ERROR: ...`,sigil 是红 ``
- 验证:`!false``!ls /nonexistent`
- 来源:`src/tui/backend.rs:106` (`success = !output.starts_with("ERROR: ")`)
- 状态:[ ]

### G.4 ID 编号
- 期望:ToolCall/ToolResult id 形如 `ui-bash-<N>`,N 在 backend 启动后递增
- 来源:`src/tui/bash.rs:34` (`format!("ui-bash-{n}")`)
- 状态:[ ]

---

## H. Tool call / Tool result / Diff 渲染

### H.1 工具名称 → spinner 动词
- 期望:`read_file` / `list_dir` / `search_files` → "Reading";`apply_patch` / `write_file` → "Editing";`run_shell` → "Running";其它 → "Calling tool"
- 验证:观察 turn 在跑时 status bar 附近的 spinner 文本
- 来源:`src/tui/app.rs:1630-1638` (`verb_for_tool`)
- 状态:[ ]

### H.2 apply_patch 自动展开 Diff
- 期望:ToolCall 来时如果是 `apply_patch`**同时**推一个 Diff 块(解析 V4A 格式)
- 验证:触发 apply_patch
- 来源:`src/tui/app.rs:704-708`
- 状态:[ ]

### H.3 write_file 走 stub
- 期望:write_file 成功时推 Diff 块(hunks=空 + path),****推 ToolResult 块;失败时正常 ToolResult
- 验证:`write_file` vs `apply_patch` 看 transcript 区别
- 来源:`src/tui/app.rs:723-731`
- 状态:[ ]

### H.4 V4A patch 解析
- 期望:解析 `*** Update File: path` / `*** Add File: path` / `@@` 锚 / `+` `-` ` ` 前缀;多 section 只取第一个;无 `***` 或 hunks 空时返回 None
- 来源:`src/tui/app.rs:1645-1721` (`parse_apply_patch_input` / `parse_v4a_patch`)
- 状态:[ ]

### H.5 ToolResult 展开
- 期望:Ctrl+E 在 buffer 空时切换最近一个 ToolResult 的 expanded(同时点过 Update Diff 路径不影响)
- 验证:长 output 的 ToolResult,按 Ctrl+E 看是否全展开;再按一次看是否折叠
- 来源:`src/tui/app.rs:249-256` (Ctrl+E), `src/tui/app.rs:1579-1586` (`toggle_last_expandable`)
- 状态:[ ]

---

## I. @file 补全 (Goal 158)

### I.1 触发
- 期望:在 **Prompt 模式**下输入 `@` 立即切到 AtFile 模式(`@` 被插入 buffer,光标之后)
- 验证:在 Prompt 模式输 `@`,看 indicator 是不是变 ``(保持)但 input box title 变 ` @File `
- 来源:`src/tui/app.rs:546-550`, `src/tui/input_state.rs:73-77`
- 状态:[ ]

### I.2 候选列表
- 期望:进入 AtFile 后立即有初始候选(按 glob 列工作区文件,最多 12 条)
- 验证:在一个有 10+ 个文件的项目里输 `@` 看 popup
- 来源:`src/tui/app.rs:1065-1070` (`enter_atfile_mode`), `src/tui/completion.rs:91-115` (`glob_workspace_files`)
- 状态:[ ]

### I.3 过滤
- 期望:继续输字符(atfile_query)实时过滤;最多 12 条;同深度内按文件名排序
- 验证:`@src` 看 popup 是否只含 `src/...` 路径
- 来源:`src/tui/completion.rs:54-79` (`search_history` 类逻辑), `src/tui/app.rs:1073-1085` (`refresh_atfile_suggestions`)
- 状态:[ ]

### I.4 选中导航
- 期望:↑↓ 移动 atfile_selected;Enter/Tab 提交选中(替换 `@<query>``@<chosen>`);Esc 取消但**保留 `@<query>` 在 buffer**
- 验证:↑/↓ 选,Enter 看 buffer 是否变 `@<完整路径>`
- 来源:`src/tui/app.rs:1118-1176` (`handle_atfile_key`)
- 状态:[ ]

### I.5 退出与删除
- 期望:Backspace 在 query 空时 → 删 `@` 并退 AtFile;query 非空时 → 删最后字符并刷新候选
- 验证:`@src` 然后 Backspace 看是否变 `@` + 候选恢复全量
- 状态:[ ]

### I.6 排除规则
- 期望:候选****包含:`.` 开头的隐藏目录、`target/``node_modules/`;最多走 3 层目录
- 验证:在 target/ 下故意放文件,输 `@` 看是否出现
- 来源:`src/tui/completion.rs:135-138` (skip)
- 状态:[ ]

---

## J. Ctrl+R 历史搜索 (Goal 160)

### J.1 触发
- 期望:Prompt 模式按 Ctrl+R 切到 HistorySearch 模式(input box title 变 `🔍 History Search`- 验证:按 Ctrl+R 看 input box title
- 来源:`src/tui/app.rs:284-300`, `src/tui/input_state.rs:74-77`
- 状态:[ ]

### J.2 初始匹配
- 期望:进入时 hsearch_query 为空,匹配所有历史(最近优先,最多 12 条)
- 来源:`src/tui/app.rs:1183-1188` (`enter_history_search_mode`)
- 状态:[ ]

### J.3 输入过滤
- 期望:输入字符追加到 hsearch_query;匹配规则 = 大小写不敏感,前缀匹配优先于子串匹配,结果按历史时间倒序
- 验证:输 `co` 看是否 `compact``cost` 优先
- 来源:`src/tui/completion.rs:54-79` (`search_history`)
- 状态:[ ]

### J.4 选中导航
- 期望:↑↓ 移动 hsearch_selected;Enter 用选中条目**覆盖** prompt.buffer(不追加);Esc 退出且****改 buffer
- 验证:↑/↓ 选,Enter 看 buffer 是否被覆盖
- 来源:`src/tui/app.rs:1219-1266` (`handle_history_search_key`)
- 状态:[ ]

### J.5 二次 Ctrl+R
- 期望:HistorySearch 模式下再按 Ctrl+R → 移到下一个匹配(循环)
- 验证:多个匹配时连续 Ctrl+R 看 hsearch_selected 变化
- 来源:`src/tui/app.rs:290-297`
- 状态:[ ]

### J.6 Backspace
- 期望:query 空时 Backspace → 退 HistorySearch(不改 buffer);非空时 → 删最后字符并刷新
- 状态:[ ]

---

## K. Plan mode 全流程

> 三种 plan 相关变体:plan 模式开关、plan 提案、plan-mode entry 请求。

### K.1 /plan on/off (前置开关)
- 期望:见 C.8;状态栏会显示 "plan-first" 段
- 状态:[ ]

### K.2 PlanProposed 流程 (agent 提议 plan)
- 期望触发:runtime 推 `UiEvent::PlanProposed { plan_text, tool_calls }`
- 期望行为:
  1. 推一个 inline `PlanProposal` 块(**不弹 modal**)到 transcript,框内含 plan_text + "Pending tools (N):" + 工具列表 + 提示 `[y/Enter] Approve [n/Esc] Reject [e] Edit`
  2. `plan_awaiting_approval = true` → 状态栏出现 `plan: y/n`
  3. 用户按 y/Enter → 弹 modal(如有)+ 发 `UserAction::ConfirmPlan`
  4. 用户按 n/Esc → 弹 modal + 发 `UserAction::RejectPlan("user rejected")`
  5. 用户按 e → 把 plan_text 复制到 prompt buffer(Prompt 模式),弹 modal,让用户改完再发
  6. 等 runtime 推 `UiEvent::PlanConfirmed` / `PlanRejected` → 推对应 System 块
- 验证:开 `/plan on`,让 agent 提议一个 plan
- 来源:`src/tui/app.rs:779-808, 1355-1379, 955-967`
- 状态:[ ]

### K.3 PlanModeRequested 流程 (agent 调 `request_plan_mode`)
- 期望触发:runtime 推 `UiEvent::PlanModeRequested { reason }`
- 期望行为:
  1. 推 inline `PlanModeRequest { reason, approved: None }` 块(蓝色边框,yellow reason)
  2. `plan_mode_request_pending = true`**不弹 modal**,但所有键走 `handle_plan_mode_request_key`  3. y/Enter → `plan_mode_request_pending = false` + 发 `UserAction::ApprovePlanMode`
  4. n/Esc → 同上 + 发 `UserAction::RejectPlanMode("user skipped")`
  5. 等 runtime 推 `PlanModeApproved` / `PlanModeRejected` → 把对应 block 的 `approved` 字段置 true/false,渲染层据此显示 "✓ Plan mode allowed" 或 "✗ Plan mode skipped"
- 验证:让 agent 调 `request_plan_mode` 工具
- 来源:`src/tui/app.rs:811-839, 1388-1400`, `src/tui/ui/transcript.rs:447-548`
- 状态:[ ]

### K.4 Inline vs Modal 区别
- 期望:PlanProposal ****用 modal,直接 inline 渲染(Fix-E 的设计选择);PlanModeRequest 也不弹 modal,但**键路由上**等同 modal
- 验证:触发时 transcript 一直可见,背景不被 Clear
- 来源:`src/tui/app.rs:789-794` 注释, `src/tui/ui/transcript.rs:290-394` 渲染
- 状态:[ ]

---

## L. Permission 请求 (Goal 161)

### L.1 触发
- 期望:runtime 推 PermissionRequest 到 `perm_rx` 侧通道
- 来源:`src/tui/backend.rs:186-212` (`TuiPermissionHook`)
- 状态:[ ]

### L.2 渲染
- 期望:推 `Modal::Confirm`**实际不**,当前代码看到的是单独的 `pending_permission` 字段,渲染逻辑在 `ui/chat.rs`(需要单独查);先看 `handle_permission_key` 验证键位
- 备注:**待查**`ui/chat.rs` 是怎么处理 pending_permission 的
- 状态:[ ]

### L.3 键位
- 期望:y/Y/Enter → allow(reply.send(true));n/N/Esc → deny(reply.send(false));a/A → allow + 把 tool name 加进 auto_allowed_tools;其它键无反应
- 验证:`/permissions on`,触发 tool call,看 modal 行为
- 来源:`src/tui/app.rs:1296-1310` (`handle_permission_key`)
- 状态:[ ]

### L.4 Auto-allow
- 期望:工具进 `auto_allowed_tools` 后,同一会话内该工具的 PermissionRequest 直接 reply(true) 不弹 modal
- 验证:a 一次后,下一次同工具不应再弹
- 来源:`src/tui/app.rs:1274-1278` (`set_pending_permission`)
- 状态:[ ]

### L.5 Hook 关闭
- 期望:`/permissions off` → 清空 `auto_allowed_tools`、deny 当前 pending permission
- 验证:在 modal 打开时 `/permissions off`,看 modal 是否消失(reply false 后代码上没显式 pop pending_permission,下次 set 会替换)
- 来源:`src/tui/commands.rs:374-383`
- 状态:[ ]

### L.6 Hook off 时全局放行
- 期望:`permission_hook_enabled` 为 false 时,`TuiPermissionHook::ask_permission` 直接 return true(不弹 modal)
- 来源:`src/tui/backend.rs:197-200`
- 状态:[ ]

---

## M. Goal loop (Goal 168)

### M.1 启动
- 期望:`/goal <cond>` → 发 `UserAction::SetGoal { condition, max_turns: 20 }`(或解析的 N)
- 验证:`/goal make tests pass` Enter
- 状态:[ ]

### M.2 状态显示
- 期望:runtime 推 `UiEvent::GoalContinuing { reason, turns }` → 推 "Goal continuing (turn N): ..." System 块
- 验证:观察 transcript
- 来源:`src/tui/app.rs:847-856`
- 状态:[ ]

### M.3 完成
- 期望:runtime 推 `UiEvent::GoalAchieved { condition, turns }` → 推 "Goal achieved after N turns: ..." System 块 + `active_goal = None`
- 验证:满足条件后看 transcript
- 来源:`src/tui/app.rs:857-862`
- 状态:[ ]

### M.4 清除
- 期望:`UiEvent::GoalCleared``/goal clear` → 推 "Goal cleared." System 块 + `active_goal = None`
- 状态:[ ]

---

## N. MCP 服务器 (Goal 173)

### N.1 列表
- 期望:`/mcp` → 发 `UserAction::ListMcpServers` → runtime 异步拉 → `UiEvent::McpServersLoaded { entries }` → 弹 McpServers modal
- 验证:配置 MCP server 后 `/mcp`
- 来源:`src/tui/backend.rs:453-479`
- 状态:[ ]

### N.2 渲染项
- 期望:每条 entry 显示 `▶ <bullet> <name> (<transport>)`;transport = `http`(有 url)/ `stdio`(有 command)/ `unknown`
- 状态:[ ]

### N.3 不可切换
- 期望:McpServers modal 只看,**无启用/禁用 toggle**`enabled` 字段固定为 true,UI 没改入口)
- 备注:见 D.9
- 状态:[ ]

---

## O. 技能命令 (Goal 169)

### O.1 加载路径
- 期望:优先级 `<workspace>/.recursive/skills/*.md` > `~/.recursive/skills/*.md`;同 name 后者被忽略
- 验证:两个目录放同名 skill,看谁赢
- 来源:`src/tui/skill_commands.rs:83-106` (`load`)
- 状态:[ ]

### O.2 Frontmatter 解析
- 期望:支持 `name` / `description` / `aliases`([a,b] 或 a)/ `argument_hint`(引号可选)/ `allowed_tools`([a,b]);不支持的字段忽略
- 验证:放一个有完整 frontmatter 的 skill
- 状态:[ ]

### O.3 名称净化
- 期望:name 全部小写;非字母数字非 `-` 替换为 `-`;首尾 `-` 去掉;空名 → 跳过
- 验证:name = "My Skill!" 看是否变 "my-skill"
- 来源:`src/tui/skill_commands.rs:233-248`
- 状态:[ ]

### O.4 参数展开
- 期望:正文中的 `$ARGUMENTS``{{args}}` 都替换为用户参数;空参数 → 替换为空字符串(`"$ARGUMENTS"` → 消失)
- 验证:skill 是 "Echo: $ARGUMENTS",`/test foo` 看 prompt
- 状态:[ ]

### O.5 内置命令优先
- 期望:skill 名字和内置命令重名时,**内置赢**(skill 被 shadow)
- 验证:内置有 `help`,放一个 `help.md` skill,看 `/help` 走哪个
- 来源:`src/tui/commands.rs:232-240` (`lookup_skill`)
- 状态:[ ]

### O.6 调起后行为
- 期望:把展开后的 prompt 作为 `User` 块入 transcript + 发 `UserAction::RunSkillPrompt { prompt }` 走 LLM
- 验证:调 `/test hello` 看 transcript
- 来源:`src/tui/app.rs:655-668`
- 状态:[ ]

---

## P. 中断 / 退出 / 重置

### P.1 中断 turn (Esc / Ctrl+C 在 turn 跑时)
- 期望:发 `UserAction::Interrupt` → backend 设 `cancel_flag` → tokio select 唤醒 → `handle.abort()` → 等 `handle.await` → 推 `UiEvent::Interrupted` → TUI 推 "[Interrupted]" System 块 + 收尾(truncate transcript 到 pre-turn)
- 验证:发起一个慢 turn(长 prompt),按 Esc,看 transcript
- 来源:`src/tui/backend.rs:552-599` (`run_turn_select_loop`), `src/tui/app.rs:870-877`
- 状态:[ ]

### P.2 中断超时
- 期望:Esc/Ctrl+C 后 < 200ms 内可观察 interrupt
- 备注:Goal 170 的目标
- 状态:[ ]

### P.3 完全退出路径
- 期望:以下任一触发 `should_quit = true`  - `q`(空 buffer)
  - `/exit` `/quit` `/q` 命令
  - Ctrl+C 第一次在 modal/turn/buffer 都无 → arm hint;**2s 内**第二次 → 真退出
  - 终端 Ctrl+D(crossterm 默认 EOF → `Event::Key(Char('d'))` 不在代码处理,**实际可能不退出**——`mod.rs:331-411` 看不到 Ctrl+D 分支)
  - Confirm modal y + on_yes=Exit
- 验证:分别测这几种
- 来源:`src/tui/app.rs:464-505, 371-374, 351-354`
- 状态:[ ]

### P.4 Reset transcript
- 期望:`/clear` → 清 blocks(除 1 个 "Conversation cleared.")+ 重置 UsageStats + reset turn_count = 0
- 来源:`src/tui/app.rs:990-999`
- 状态:[ ]

### P.5 RAW 模式清理 (raw mode 退出)
- 期望:`run()` 退出路径中 disable_raw_mode / LeaveAlternateScreen / DisableMouseCapture 都执行
- 备注:当前**没看到 panic-safe RAII**——panic 时终端可能卡死 raw mode
- 验证:故意在 TUI 跑时 `Ctrl+Z` 挂起看 terminal 状态
- 来源:`src/tui/mod.rs:36-92`
- 状态:[ ]

---

## Q. 主题 (Goal 174)

### Q.1 默认主题
- 期望:`dark` (DARK 常量)
- 来源:`src/tui/app.rs:189`
- 状态:[ ]

### Q.2 主题列表
- 期望:至少 dark / light / solarized(看 `ALL_THEMES`- 验证:`/theme`,看输出
- 来源:`src/tui/ui/theme.rs::ALL_THEMES`
- 状态:[ ]

### Q.3 主题切换即时
- 期望:`/theme light` → 立即应用到所有渲染(无重启)
- 验证:切换看 status bar / transcript 颜色
- 状态:[ ]

---

## R. 已知限制 / 半成品

> 这些是当前代码里能看出来的限制,**不是 bug**,只是没做。

### R.1 Markdown 渲染简化
- 期望:assistant 块只走 `render_inline`(粗 / 斜 / 代码 / 链接)+ `render_table`****做完整 markdown(标题、列表、引用等)
- 验证:让 agent 输出 `# 标题` / `- 列表`,看 transcript
- 来源:`src/tui/ui/markdown.rs`, `src/tui/ui/transcript.rs:127-143`
- 状态:[ ]

### R.2 无 syntax highlighting
- 期望:代码块不染色
- 备注:docs/tui-fake-cc-gap.md 列为候选
- 状态:[ ]

### R.3 Transcript 全量渲染
- 期望:>500 块时性能可能下降(不虚拟滚动)
- 备注:docs/tui-fake-cc-gap.md 列为简化版
- 状态:[ ]

### R.4 Resize 不显式触发 redraw
- 期望:依赖 crossterm `Resize` 事件自动重绘;无 Ctrl+L 显式重绘
- 备注:docs/tui-fake-cc-gap.md 列为 🟡
- 状态:[ ]

### R.5 鼠标支持
- 期望:仅有滚轮(ScrollUp/ScrollDown ±3 行),无点击、选择
- 来源:`src/tui/mod.rs:94-106` (`handle_mouse`)
- 状态:[ ]

### R.6 Image / 拖拽 / OSC 8 / 链接
- 期望:均未实现
- 状态:[N/A]

### R.7 Hook 进度显示
- 期望:`HookStarted` / `HookProgress` / `HookFinished` 推 System 块(不是 modal);`HookProgress` 会更新最近一个 `⚡ hook` System 块的内容
- 验证:装一个 hook 跑一下
- 来源:`src/tui/app.rs:895-944`
- 状态:[ ]

### R.8 无 / 复制、/复制消息、/diff、/export
- 状态:[N/A]

---

## 待办:列出还没验证或需要补的

- [x] L.2 节:去 `ui/chat.rs` 看 pending_permission 怎么渲染 → 见 L.2.1-L.2.6
- [x] ui/spinner.rs / ui/splash.rs / ui/diff.rs / ui/markdown.rs 单独成节 → 见 S 节
- [x] ui/theme.rs 三个主题具体颜色(dark / light / solarized)→ 见 S.6
- [ ] **User 需要补充**: 任何"设计意图上是这样"但代码里看不出来的东西(比如某段设计理由、某段为啥这么做)
- [ ] **User 需要补充**: 当前代码已经覆盖的"真"行为,有没有你**不打算**告诉用户/不打算展示的(如某些 hack、调试残留)

---

## S. 渲染层细节(补遗)

> 这节把之前没单独成节但实际在用的小模块行为列出来。
> 来源:`src/tui/ui/{chat,splash,spinner,diff,markdown,theme,command_menu}.rs`

### S.1 Chat 屏渲染顺序(从下到上)
- 期望:依次绘制
  1. Messages panel(顶部,borders + scroll)
  2. Todo panel(只有当 `current_todos` 非空时,夹在 messages 和 status 之间)
  3. Status bar(底部 1 行)
  4. Plan approval banner / Plan mode request banner(仅 pending 时 1 行)
  5. Input + footer hint
  6. Command menu popup(覆盖在 input 上方)
  7. @file completion popup
  8. Ctrl+R history search popup
  9. Permission modal(覆盖一切)
  10. 通用 modals(`Modal::Help` 等)—— 最后绘制,层级最高
- 验证:同时有 permission modal 和 McpServers modal 不会冲突(应该不会,因为 permission 是 `pending_permission` 字段而非 `modals` 栈)
- 来源:`src/tui/ui/chat.rs:38-153`
- 状态:[ ]

### S.2 滚动算法
- 期望:`scroll_offset` 是"距离底部多少行";**不是**"从顶部跳过多少行"
- 期望:实际滚动用 `effective_scroll = max_scroll - min(scroll_offset, max_scroll)`,保证 `scroll_offset = 0` 时永远贴底
- 期望:`max_scroll``Line::width()` 估算(post-wrap 行数,不是 logical 行数)
- 备注:长 transcript 滚到底后,**`scroll_offset == 0` 自动滚到底**(见 E.11)
- 来源:`src/tui/ui/chat.rs:80-107`
- 状态:[ ]

### S.3 Todo panel
- 期望:有 `current_todos` 时,在 messages 之下、status bar 之上多一段;最多 6 条,超出截断
- 期望:图标 ``(绿) = Completed,``(黄) = InProgress,``(暗灰) = Pending,``(暗灰) = Cancelled
- 期望:InProgress 显示 `active_form` 字段;其它显示 `content` 字段
- 期望:标题 `" Tasks (<completed>/<total> done) "`
- 验证:让 agent 调 `todo_write` 看 panel
- 来源:`src/tui/ui/chat.rs:155-195`
- 状态:[ ]

### S.4 Plan approval banner (Fix-E)
- 期望:1 行高;`plan_awaiting_approval` 时显示
- 期望:内容格式 `" ⚡ Plan awaiting approval — [y/Enter] Approve  [n/Esc] Reject  [e] Edit "`
- 期望:背景色 黄;`[y/Enter]` 绿底、`[n/Esc]` 红底、`[e]` Cyan 底,所有键帽黑字加粗
- 验证:`/plan on` + 让 agent 提个 plan
- 来源:`src/tui/ui/chat.rs:197-249` (`render_plan_approval_banner`)
- 状态:[ ]

### S.5 Plan mode request banner (Goal 202)
- 期望:1 行高;`plan_mode_request_pending` 时显示(且 `plan_awaiting_approval` 不显示时——优先级看 `chat.rs:125-129`)
- 期望:内容格式 `" ⓘ Plan mode request — [y/Enter] Allow  [n/Esc] Skip — execute directly "`
- 期望:背景色 蓝;`[y/Enter]` 绿底、`[n/Esc]` 红底
- 验证:让 agent 调 `request_plan_mode` 工具
- 来源:`src/tui/ui/chat.rs:251-294` (`render_plan_mode_request_banner`)
- 状态:[ ]

### S.6 Splash 屏内容
- 期望:居中 ASCII logo:
  ```
      ╱╲    Recursive Agent
     ╱  ╲   ─────────────────
    ╱ ╱╲ ╲  v0.4.0
    ╲ ╲╱ ╱
     ╲  ╱   Self-improving AI agent
      ╲╱    in Rust
  
     Press any key to continue...
  ```
- ⚠️ **版本号硬编码为 v0.4.0**,但 `Cargo.toml` 现在是 v0.6.0 — 这是已知的过期,要不要改?
- 验证:启动后看 splash
- 来源:`src/tui/ui/splash.rs:12-25`
- 状态:[ ]

### S.7 Spinner 动画
- 期望:turn 跑时,messages 末尾追加一行 `<spinner> <verb> X.Xs`
- 期望:10 帧 braille:`⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏`,每 50ms tick 一次,每帧 100ms
- 期望:verb 默认 "Thinking",见 H.1 的工具 → verb 映射
- 验证:发起 turn 看 spinner 在动
- 来源:`src/tui/ui/spinner.rs:10` (`FRAMES`), `src/tui/app.rs:168` (spinner_frame 在主循环 +1)
- 状态:[ ]

### S.8 Diff 渲染细节
- 期望:每行格式 `    │ <sigil> <text>`,sigil `+` Green、`-` Red、` ` Gray
- 期望:header `  📝 <path>`,📝 Magenta、path White
- 期望:无 hunks(如 write_file 合成)→ 单行 `    │ Updated <path>`,Gray italic
- 验证:触发 apply_patch
- 来源:`src/tui/ui/diff.rs`
- 状态:[ ]

### S.9 Markdown 渲染(single-line `render_inline`)
- 期望:支持粗体 `**...**`(LightCyan bold)、斜体 `*...*`/`_..._`(italic)、行内代码 `` `...` ``(LightYellow)、标题 `# ` `## ` ... `###### `(LightCyan bold)、无序列表 `- ` `* ` `+ `(替换为 `` LightYellow)
- 期望:Markdown 表格(连续 `|...` 行)用 `render_table` 单独走带边框的格式(top/header/divider/data/bottom 五行,header LightCyan bold)
- 验证:让 agent 输出 markdown 文本
- 来源:`src/tui/ui/markdown.rs::render_inline / render_table / is_table_line`
- 状态:[ ]

### S.10 完整 Markdown 渲染(`render_markdown`,Goal 172)
- 期望:用 `pulldown-cmark` 完整解析;支持粗/斜/行内代码/fenced code block(每行 `` Cyan 前缀)/ 有序列表(数字 + `.`)/ 无序列表/ 水平线 `---`(填满 wrap_width)/ 段落
- 期望:fenced code block 的 language tag 若 syntect 识别 → syntax highlight(用 `base16-ocean.dark` 主题);不识别 → 整行 LightYellow
- 期望:**不支持**:HTML/InlineHtml/InlineMath/DisplayMath/FootnoteReference/TaskListMarker —— 直接忽略
- 期望:**不支持**:链接/图片/引用/删除线/原生 markdown 表格(被外层 `render_table` 接管,这里 fall through)
- 期望:行级 heading 解析在 `Tag::Heading` 注释说"rendered as bold+cyan",****区分 h1/h2/h3
- 验证:让 agent 输出复杂 markdown
- 备注:**当前似乎未被 chat.rs 调用**——chat.rs 用的是 `render_blocks``render_block``render_assistant` 里的 `render_inline`,**不是** `render_markdown`。这个 `render_markdown` 可能是给别的场景(如 `/journal` 或 future),需要确认是否真在用
- 来源:`src/tui/ui/markdown.rs:262-482` (`render_markdown`)
- 状态:[ ]

### S.11 Theme (3 个内置,21 个 color field)
- 期望字段(全部):
  ```
  name, status_bg, status_fg, status_mode_fg, status_cost_fg, status_dim_fg,
  input_border, input_prompt_fg, input_bash_fg, input_note_fg, input_command_fg, input_atfile_fg,
  user_bar, assistant_bar, system_bar,
  tool_call_icon, tool_ok_fg, tool_err_fg, code_fg,
  diff_add, diff_del
  ```
- 期望默认 dark;`/theme` 切换即时
- 验证:`/theme light` 看每段颜色变
- 来源:`src/tui/ui/theme.rs:14-40`
- 状态:[ ]

### S.12 Dark 主题(当前默认)
- 期望配色:status bar 背景 DarkGray,连接段 Green,model Cyan,token White,cost Cyan,dim DarkGray
- 期望 input_border DarkGray;指示符 Prompt Cyan, Bash Yellow, Note Green, Command Magenta, AtFile Cyan
- 期望 user_bar Blue, assistant_bar Green, system_bar DarkGray
- 期望 tool_call_icon Yellow, tool_ok Green, tool_err Red, code Cyan
- 期望 diff_add Green, diff_del Red
- 来源:`src/tui/ui/theme.rs:44-66`
- 状态:[ ]

### S.13 Light 主题
- 期望:背景变浅,前景变深,关键色保持语义(Green = ok, Red = err)
- 备注:**当前 chat.rs 渲染代码里大部分地方还是直接用 `Color::Xxx` 常量,而不是从 `app.theme`**——只有 `command_menu` 等少数地方实际用了 theme。Light 主题可能"看起来不像 light",因为大部分颜色硬编码
- 验证:`/theme light`,观察 status bar / input 框 / transcript 哪些变了哪些没变
- 来源:`src/tui/ui/theme.rs:68-90`
- 状态:[ ]

### S.14 Solarized 主题
- 期望:使用 Solarized 调色板的标准 RGB 值(status_bg = #073642, status_fg = #839496 等)
- 备注:同 S.13,大部分硬编码颜色不会切
- 来源:`src/tui/ui/theme.rs:92-114`
- 状态:[ ]

### S.15 Command menu popup 渲染
- 期望:在 input 框**上方**弹出(2 borders + 最多 8 行,popup 高度 = N+2)
- 期望:popup 宽度 = `input_width.clamp(20, 60)`
- 期望:每行格式 ` /<name>   <summary> `,skill 行尾追加 ` [skill]` 绿色角标
- 期望:背景 Black,边框 DarkGray,标题 "Commands"
- 期望:选中行 = 黑字黄底加粗
- 验证:`/co` 输到一半,看 popup
- 来源:`src/tui/ui/command_menu.rs:117-175`
- 状态:[ ]

### S.16 AtFile popup 渲染
- 期望:同位置(input 上方),最多 8 行
- 期望:每行 ` <path> `,选中行 = 黑字 Cyan 底加粗
- 期望:标题格式 ` @files  query: "..." `,边框 Cyan
- 验证:输 `@src` 看 popup
- 来源:`src/tui/ui/command_menu.rs:179-220`
- 状态:[ ]

### S.17 HistorySearch popup 渲染
- 期望:同位置,匹配为空时也显示,显示 "(no matches)" 暗灰 italic 一行
- 期望:每行 ` <entry> `,entry 超过 60 字符截断到 57 + ``
- 期望:选中行 = 黑字 LightGreen 底加粗
- 期望:标题 ` 🔍 <query> `,边框 LightGreen
- 验证:Ctrl+R 输 `xx` 看无匹配提示
- 来源:`src/tui/ui/command_menu.rs:224-280`
- 状态:[ ]

### S.18 Permission modal 渲染
- 期望:屏幕**正中**居中(不是像普通 modal 那样 70% × 70%):宽度 = `min(frame.width - 8, 72)`,高度固定 8
- 期望:背景清屏(整个 modal_area 被 Clear 覆盖)
- 期望:Yellow 边框,标题 `⚠ Permission Request` Yellow 加粗
- 期望:内容:
  - 1 空行
  - ` Tool: <tool_name> `(Tool: 黑字黄底,name 白字)
  - ` Args: <preview> `(Args: 暗灰,preview 白字;preview > 60 字符截到 59 + ``;空 preview 显示 `(no arguments)`)
  - 1 空行
  - `` 横线(填满 modal 宽度 - 2)
  - `  [Y]/[Enter] Allow  [N]/[Esc] Deny  [A] Allow All`
- 验证:`/permissions on` + 触发 tool call
- 来源:`src/tui/ui/command_menu.rs:287-378` (`render_permission_modal`)
- 状态:[ ]

### S.19 终端原生光标定位
- 期望:input 框渲染时调用 `frame.set_cursor_position`,光标落在正确字符的下一列
- 期望:CJK / 全角字符按 2 列宽计算(用 `unicode_width::UnicodeWidthStr`),不是 1 列
- 验证:输入 "你好" 在第二个字后面,看光标是不是在 4 列后
- 来源:`src/tui/ui/input.rs:101-110, 156-163` (`cursor_visual_position`)
- 状态:[ ]

### S.20 输入框 `>`/边框/标题
- 期望:输入框有四边边框(borders),title 按 mode 变:
  - `Input ` (Prompt)
  - ` Bash ` (Bash)
  - ` Note ` (Note)
  - ` Command ` (Command)
  - ` @File ` (AtFile)
  - ` 🔍 History Search ` (HistorySearch)
- 期望:每行首字符 indicator(在 box 内部左侧)按 mode 变:`` `!` `#` `/`
- 验证:看不同 mode 下输入框顶部 title
- 来源:`src/tui/ui/input.rs:132-141, 81-88, 119-129`
- 状态:[ ]

### S.21 Input footer hint(每行模式下的按键提示)
- 期望:
  - Prompt: `⏎ submit  shift+tab mode  ↑↓ history  ctrl+b/f or wheel scroll  esc clear`
  - Bash: `⏎ run shell  ...`
  - Note: `⏎ save note  ...`
  - Command: `⏎ run command  tab autocomplete  ↑↓ history  ctrl+b/f or wheel scroll`
  - AtFile: `⏎/tab confirm  ↑↓ select  backspace edit  esc cancel`
  - HistorySearch: `⏎ confirm  ↑↓ select  ctrl+r next  backspace edit  esc cancel`
- 验证:看 input 框下方 1 行的 hint
- 来源:`src/tui/ui/input.rs:166-185` (`footer_hint`)
- 状态:[ ]

---

## T. 跨节 / 容易漏掉的杂项

### T.1 鼠标支持
- 期望:只有滚轮。ScrollUp 加 scroll_offset 3,ScrollDown 减 3;其它鼠标事件忽略
- 验证:用滚轮在 transcript 滚
- 来源:`src/tui/mod.rs:94-106`
- 状态:[ ]

### T.2 Workspace root 解析
- 期望:`$RECURSIVE_WORKSPACE` > `std::env::current_dir()` > `.`
- 验证:`RECURSIVE_WORKSPACE=/tmp cargo run``!pwd` 输出
- 来源:`src/tui/bash.rs:19-25` (`resolve_workspace_root`)
- 状态:[ ]

### T.3 离线模式行为
- 期望:runtime 构建失败时,backend 推一个 `UiEvent::Error { message: reason }`,然后所有 UserAction 都不实际执行(只 echo Error)
- 验证:在没配 API key 的环境跑 `cargo run`,看 transcript
- 来源:`src/tui/backend.rs:275-281, 348-353, 411-415, 520-525` (Offline 分支)
- 状态:[ ]

### T.4 写权限 `should_quit` 路径
- 期望:任何**接受** `UserAction::Shutdown` 之前,bash / skill / turn 任务都被 cancel 或 finish
- 验证:正在 bash 跑时按 Ctrl+C 两次,看进程是否干净退出
- 来源:`src/tui/backend.rs:239` (Shutdown 直接 break loop)
- 状态:[ ]

### T.5 启动时模型自动探测副作用
- 期望:`App::new()` 会读 `Config::from_env()` 拿 workspace,这会读 env vars / `~/.recursive/config.toml` —— 启动慢的话可能是这里
- 备注:不是 bug,但调试时要知道
- 来源:`src/tui/app.rs:144-150` (`App::new` 启动时调 `Config::from_env`)
- 状态:[ ]

---

## 怎么用本文档(更新版)

1. **逐项跑一遍**,在状态栏打勾 `[x]` / `[~]` / `[N/A]`
2.`[~]` 的 = 你**不想要**当前行为 → 改代码 → 改完回这份文档更新 → 重新验证
3.`[x]` 达到 ~80% 时 → 这份文档**就是 ground truth** → 那时候再开始铺单测
4. 测试**应该锁住这份文档描述的行为**,不是"代码现在的行为"
5. **重要**:本文档基于 `src/tui/` 实际代码抽出,****包含"我猜代码应该这样但其实没做"的内容。如果有**设计意图**是代码看不出来但你**期望**的(比如某段"本来该是 X 但代码没做"),**请在 T 节"待补"里加**,我来加进文档
6. **S.10** `render_markdown` 这个函数我搜了 chat.rs,**没找到调用**——你确认下是不是 dead code 还是未来要用
7. **S.6** Splash 屏硬编码 `v0.4.0`,实际 `Cargo.toml``v0.6.0`,这个是不是 bug?
8. **S.13/S.14** Light / Solarized 主题只覆盖了 `theme.rs` 里 21 个字段;**当前 chat.rs 渲染代码大部分还是硬编码 `Color::Xxx`,不是从 `app.theme`**——所以 `/theme light` 视觉变化可能很有限。这是真 bug 吗?还是只是"没做完"?

## 怎么用本文档

1. **逐项跑一遍**,在状态栏打勾 `[x]` 或标 `[~]` 配备注
2.`[~]` 的 = 你**不想要**当前行为 → 改代码 → 改完回这份文档更新 → 重新验证
3.`[x]` 的达到 ~80% 时 → 这份文档就是 ground truth → 才开始铺单测
4. 测试应该锁住**这份文档描述的行为**,不是"代码现在的行为"