cxstatusline 0.1.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2825 @@
1
+ diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
2
+ index 45fca1b150..7ba1057fa1 100644
3
+ --- a/codex-rs/Cargo.lock
4
+ +++ b/codex-rs/Cargo.lock
5
+ @@ -416,7 +416,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
6
+
7
+ [[package]]
8
+ name = "app_test_support"
9
+ -version = "0.0.0"
10
+ +version = "0.154.0"
11
+ dependencies = [
12
+ "anyhow",
13
+ "base64 0.22.1",
14
+ @@ -2040,7 +2040,7 @@ dependencies = [
15
+
16
+ [[package]]
17
+ name = "codex-agent-extension"
18
+ -version = "0.0.0"
19
+ +version = "0.154.0"
20
+ dependencies = [
21
+ "anyhow",
22
+ "codex-core",
23
+ @@ -2052,7 +2052,7 @@ dependencies = [
24
+
25
+ [[package]]
26
+ name = "codex-agent-graph-store"
27
+ -version = "0.0.0"
28
+ +version = "0.154.0"
29
+ dependencies = [
30
+ "codex-protocol",
31
+ "codex-state",
32
+ @@ -2067,7 +2067,7 @@ dependencies = [
33
+
34
+ [[package]]
35
+ name = "codex-agent-identity"
36
+ -version = "0.0.0"
37
+ +version = "0.154.0"
38
+ dependencies = [
39
+ "anyhow",
40
+ "base64 0.22.1",
41
+ @@ -2087,7 +2087,7 @@ dependencies = [
42
+
43
+ [[package]]
44
+ name = "codex-agent-roles"
45
+ -version = "0.0.0"
46
+ +version = "0.154.0"
47
+ dependencies = [
48
+ "codex-config",
49
+ "codex-file-system",
50
+ @@ -2100,7 +2100,7 @@ dependencies = [
51
+
52
+ [[package]]
53
+ name = "codex-analytics"
54
+ -version = "0.0.0"
55
+ +version = "0.154.0"
56
+ dependencies = [
57
+ "codex-app-server-protocol",
58
+ "codex-git-utils",
59
+ @@ -2121,7 +2121,7 @@ dependencies = [
60
+
61
+ [[package]]
62
+ name = "codex-ansi-escape"
63
+ -version = "0.0.0"
64
+ +version = "0.154.0"
65
+ dependencies = [
66
+ "ansi-to-tui",
67
+ "ratatui",
68
+ @@ -2130,7 +2130,7 @@ dependencies = [
69
+
70
+ [[package]]
71
+ name = "codex-api"
72
+ -version = "0.0.0"
73
+ +version = "0.154.0"
74
+ dependencies = [
75
+ "anyhow",
76
+ "assert_matches",
77
+ @@ -2165,7 +2165,7 @@ dependencies = [
78
+
79
+ [[package]]
80
+ name = "codex-app-server"
81
+ -version = "0.0.0"
82
+ +version = "0.154.0"
83
+ dependencies = [
84
+ "anyhow",
85
+ "app_test_support",
86
+ @@ -2268,7 +2268,7 @@ dependencies = [
87
+
88
+ [[package]]
89
+ name = "codex-app-server-client"
90
+ -version = "0.0.0"
91
+ +version = "0.154.0"
92
+ dependencies = [
93
+ "codex-app-server",
94
+ "codex-app-server-protocol",
95
+ @@ -2295,7 +2295,7 @@ dependencies = [
96
+
97
+ [[package]]
98
+ name = "codex-app-server-daemon"
99
+ -version = "0.0.0"
100
+ +version = "0.154.0"
101
+ dependencies = [
102
+ "anyhow",
103
+ "codex-app-server-protocol",
104
+ @@ -2318,7 +2318,7 @@ dependencies = [
105
+
106
+ [[package]]
107
+ name = "codex-app-server-protocol"
108
+ -version = "0.0.0"
109
+ +version = "0.154.0"
110
+ dependencies = [
111
+ "anyhow",
112
+ "codex-app-server-protocol-noop-macros",
113
+ @@ -2352,11 +2352,11 @@ dependencies = [
114
+
115
+ [[package]]
116
+ name = "codex-app-server-protocol-noop-macros"
117
+ -version = "0.0.0"
118
+ +version = "0.154.0"
119
+
120
+ [[package]]
121
+ name = "codex-app-server-test-client"
122
+ -version = "0.0.0"
123
+ +version = "0.154.0"
124
+ dependencies = [
125
+ "anyhow",
126
+ "clap",
127
+ @@ -2378,7 +2378,7 @@ dependencies = [
128
+
129
+ [[package]]
130
+ name = "codex-app-server-transport"
131
+ -version = "0.0.0"
132
+ +version = "0.154.0"
133
+ dependencies = [
134
+ "anyhow",
135
+ "axum",
136
+ @@ -2420,7 +2420,7 @@ dependencies = [
137
+
138
+ [[package]]
139
+ name = "codex-apply-patch"
140
+ -version = "0.0.0"
141
+ +version = "0.154.0"
142
+ dependencies = [
143
+ "anyhow",
144
+ "assert_cmd",
145
+ @@ -2440,7 +2440,7 @@ dependencies = [
146
+
147
+ [[package]]
148
+ name = "codex-arg0"
149
+ -version = "0.0.0"
150
+ +version = "0.154.0"
151
+ dependencies = [
152
+ "anyhow",
153
+ "codex-apply-patch",
154
+ @@ -2462,7 +2462,7 @@ dependencies = [
155
+
156
+ [[package]]
157
+ name = "codex-async-utils"
158
+ -version = "0.0.0"
159
+ +version = "0.154.0"
160
+ dependencies = [
161
+ "pretty_assertions",
162
+ "tokio",
163
+ @@ -2471,7 +2471,7 @@ dependencies = [
164
+
165
+ [[package]]
166
+ name = "codex-attachment-store"
167
+ -version = "0.0.0"
168
+ +version = "0.154.0"
169
+ dependencies = [
170
+ "base64 0.22.1",
171
+ "codex-utils-image",
172
+ @@ -2482,7 +2482,7 @@ dependencies = [
173
+
174
+ [[package]]
175
+ name = "codex-aws-auth"
176
+ -version = "0.0.0"
177
+ +version = "0.154.0"
178
+ dependencies = [
179
+ "aws-config",
180
+ "aws-credential-types",
181
+ @@ -2497,7 +2497,7 @@ dependencies = [
182
+
183
+ [[package]]
184
+ name = "codex-backend-client"
185
+ -version = "0.0.0"
186
+ +version = "0.154.0"
187
+ dependencies = [
188
+ "anyhow",
189
+ "codex-api",
190
+ @@ -2517,7 +2517,7 @@ dependencies = [
191
+
192
+ [[package]]
193
+ name = "codex-backend-openapi-models"
194
+ -version = "0.0.0"
195
+ +version = "0.154.0"
196
+ dependencies = [
197
+ "serde",
198
+ "serde_json",
199
+ @@ -2526,7 +2526,7 @@ dependencies = [
200
+
201
+ [[package]]
202
+ name = "codex-build-info"
203
+ -version = "0.0.0"
204
+ +version = "0.154.0"
205
+ dependencies = [
206
+ "codex-install-context",
207
+ "pretty_assertions",
208
+ @@ -2539,7 +2539,7 @@ dependencies = [
209
+
210
+ [[package]]
211
+ name = "codex-bwrap"
212
+ -version = "0.0.0"
213
+ +version = "0.154.0"
214
+ dependencies = [
215
+ "cc",
216
+ "libc",
217
+ @@ -2548,7 +2548,7 @@ dependencies = [
218
+
219
+ [[package]]
220
+ name = "codex-chatgpt"
221
+ -version = "0.0.0"
222
+ +version = "0.154.0"
223
+ dependencies = [
224
+ "anyhow",
225
+ "clap",
226
+ @@ -2570,7 +2570,7 @@ dependencies = [
227
+
228
+ [[package]]
229
+ name = "codex-cli"
230
+ -version = "0.0.0"
231
+ +version = "0.154.0"
232
+ dependencies = [
233
+ "anyhow",
234
+ "app_test_support",
235
+ @@ -2663,7 +2663,7 @@ dependencies = [
236
+
237
+ [[package]]
238
+ name = "codex-client"
239
+ -version = "0.0.0"
240
+ +version = "0.154.0"
241
+ dependencies = [
242
+ "codex-http-client",
243
+ "eventsource-stream",
244
+ @@ -2676,7 +2676,7 @@ dependencies = [
245
+
246
+ [[package]]
247
+ name = "codex-cloud-config"
248
+ -version = "0.0.0"
249
+ +version = "0.154.0"
250
+ dependencies = [
251
+ "base64 0.22.1",
252
+ "chrono",
253
+ @@ -2701,7 +2701,7 @@ dependencies = [
254
+
255
+ [[package]]
256
+ name = "codex-cloud-tasks"
257
+ -version = "0.0.0"
258
+ +version = "0.154.0"
259
+ dependencies = [
260
+ "anyhow",
261
+ "chrono",
262
+ @@ -2734,7 +2734,7 @@ dependencies = [
263
+
264
+ [[package]]
265
+ name = "codex-cloud-tasks-client"
266
+ -version = "0.0.0"
267
+ +version = "0.154.0"
268
+ dependencies = [
269
+ "anyhow",
270
+ "chrono",
271
+ @@ -2749,7 +2749,7 @@ dependencies = [
272
+
273
+ [[package]]
274
+ name = "codex-cloud-tasks-mock-client"
275
+ -version = "0.0.0"
276
+ +version = "0.154.0"
277
+ dependencies = [
278
+ "chrono",
279
+ "codex-cloud-tasks-client",
280
+ @@ -2758,7 +2758,7 @@ dependencies = [
281
+
282
+ [[package]]
283
+ name = "codex-code-mode"
284
+ -version = "0.0.0"
285
+ +version = "0.154.0"
286
+ dependencies = [
287
+ "codex-code-mode-protocol",
288
+ "codex-http-client",
289
+ @@ -2781,7 +2781,7 @@ dependencies = [
290
+
291
+ [[package]]
292
+ name = "codex-code-mode-host"
293
+ -version = "0.0.0"
294
+ +version = "0.154.0"
295
+ dependencies = [
296
+ "anyhow",
297
+ "axum",
298
+ @@ -2809,7 +2809,7 @@ dependencies = [
299
+
300
+ [[package]]
301
+ name = "codex-code-mode-protocol"
302
+ -version = "0.0.0"
303
+ +version = "0.154.0"
304
+ dependencies = [
305
+ "codex-protocol",
306
+ "glob",
307
+ @@ -2827,7 +2827,7 @@ dependencies = [
308
+
309
+ [[package]]
310
+ name = "codex-code-mode-runtime"
311
+ -version = "0.0.0"
312
+ +version = "0.154.0"
313
+ dependencies = [
314
+ "base64 0.22.1",
315
+ "codex-code-mode-protocol",
316
+ @@ -2845,11 +2845,11 @@ dependencies = [
317
+
318
+ [[package]]
319
+ name = "codex-collaboration-mode-templates"
320
+ -version = "0.0.0"
321
+ +version = "0.154.0"
322
+
323
+ [[package]]
324
+ name = "codex-config"
325
+ -version = "0.0.0"
326
+ +version = "0.154.0"
327
+ dependencies = [
328
+ "anyhow",
329
+ "base64 0.22.1",
330
+ @@ -2898,7 +2898,7 @@ dependencies = [
331
+
332
+ [[package]]
333
+ name = "codex-config-schema"
334
+ -version = "0.0.0"
335
+ +version = "0.154.0"
336
+ dependencies = [
337
+ "anyhow",
338
+ "clap",
339
+ @@ -2907,7 +2907,7 @@ dependencies = [
340
+
341
+ [[package]]
342
+ name = "codex-connectors"
343
+ -version = "0.0.0"
344
+ +version = "0.154.0"
345
+ dependencies = [
346
+ "anyhow",
347
+ "arc-swap",
348
+ @@ -2929,7 +2929,7 @@ dependencies = [
349
+
350
+ [[package]]
351
+ name = "codex-connectors-extension"
352
+ -version = "0.0.0"
353
+ +version = "0.154.0"
354
+ dependencies = [
355
+ "codex-connectors",
356
+ "codex-core-plugins",
357
+ @@ -2943,7 +2943,7 @@ dependencies = [
358
+
359
+ [[package]]
360
+ name = "codex-context-fragments"
361
+ -version = "0.0.0"
362
+ +version = "0.154.0"
363
+ dependencies = [
364
+ "codex-protocol",
365
+ "codex-utils-string",
366
+ @@ -2952,7 +2952,7 @@ dependencies = [
367
+
368
+ [[package]]
369
+ name = "codex-core"
370
+ -version = "0.0.0"
371
+ +version = "0.154.0"
372
+ dependencies = [
373
+ "anyhow",
374
+ "arc-swap",
375
+ @@ -3087,7 +3087,7 @@ dependencies = [
376
+
377
+ [[package]]
378
+ name = "codex-core-api"
379
+ -version = "0.0.0"
380
+ +version = "0.154.0"
381
+ dependencies = [
382
+ "codex-analytics",
383
+ "codex-app-server-protocol",
384
+ @@ -3110,7 +3110,7 @@ dependencies = [
385
+
386
+ [[package]]
387
+ name = "codex-core-plugins"
388
+ -version = "0.0.0"
389
+ +version = "0.154.0"
390
+ dependencies = [
391
+ "anyhow",
392
+ "chrono",
393
+ @@ -3165,7 +3165,7 @@ dependencies = [
394
+
395
+ [[package]]
396
+ name = "codex-diagnostics"
397
+ -version = "0.0.0"
398
+ +version = "0.154.0"
399
+ dependencies = [
400
+ "libc",
401
+ "pretty_assertions",
402
+ @@ -3173,7 +3173,7 @@ dependencies = [
403
+
404
+ [[package]]
405
+ name = "codex-exec"
406
+ -version = "0.0.0"
407
+ +version = "0.154.0"
408
+ dependencies = [
409
+ "anyhow",
410
+ "assert_cmd",
411
+ @@ -3224,7 +3224,7 @@ dependencies = [
412
+
413
+ [[package]]
414
+ name = "codex-exec-server"
415
+ -version = "0.0.0"
416
+ +version = "0.154.0"
417
+ dependencies = [
418
+ "anyhow",
419
+ "arc-swap",
420
+ @@ -3284,7 +3284,7 @@ dependencies = [
421
+
422
+ [[package]]
423
+ name = "codex-exec-server-protocol"
424
+ -version = "0.0.0"
425
+ +version = "0.154.0"
426
+ dependencies = [
427
+ "base64 0.22.1",
428
+ "codex-file-system",
429
+ @@ -3299,7 +3299,7 @@ dependencies = [
430
+
431
+ [[package]]
432
+ name = "codex-exec-server-test-support"
433
+ -version = "0.0.0"
434
+ +version = "0.154.0"
435
+ dependencies = [
436
+ "anyhow",
437
+ "codex-exec-server",
438
+ @@ -3314,7 +3314,7 @@ dependencies = [
439
+
440
+ [[package]]
441
+ name = "codex-execpolicy"
442
+ -version = "0.0.0"
443
+ +version = "0.154.0"
444
+ dependencies = [
445
+ "anyhow",
446
+ "clap",
447
+ @@ -3332,7 +3332,7 @@ dependencies = [
448
+
449
+ [[package]]
450
+ name = "codex-experimental-api-macros"
451
+ -version = "0.0.0"
452
+ +version = "0.154.0"
453
+ dependencies = [
454
+ "proc-macro2",
455
+ "quote",
456
+ @@ -3341,7 +3341,7 @@ dependencies = [
457
+
458
+ [[package]]
459
+ name = "codex-extension-api"
460
+ -version = "0.0.0"
461
+ +version = "0.154.0"
462
+ dependencies = [
463
+ "codex-config",
464
+ "codex-context-fragments",
465
+ @@ -3359,7 +3359,7 @@ dependencies = [
466
+
467
+ [[package]]
468
+ name = "codex-extension-items"
469
+ -version = "0.0.0"
470
+ +version = "0.154.0"
471
+ dependencies = [
472
+ "codex-utils-absolute-path",
473
+ "pretty_assertions",
474
+ @@ -3371,7 +3371,7 @@ dependencies = [
475
+
476
+ [[package]]
477
+ name = "codex-external-agent-migration"
478
+ -version = "0.0.0"
479
+ +version = "0.154.0"
480
+ dependencies = [
481
+ "chrono",
482
+ "codex-analytics",
483
+ @@ -3401,7 +3401,7 @@ dependencies = [
484
+
485
+ [[package]]
486
+ name = "codex-features"
487
+ -version = "0.0.0"
488
+ +version = "0.154.0"
489
+ dependencies = [
490
+ "codex-otel",
491
+ "codex-protocol",
492
+ @@ -3414,7 +3414,7 @@ dependencies = [
493
+
494
+ [[package]]
495
+ name = "codex-feedback"
496
+ -version = "0.0.0"
497
+ +version = "0.154.0"
498
+ dependencies = [
499
+ "anyhow",
500
+ "bytes",
501
+ @@ -3436,7 +3436,7 @@ dependencies = [
502
+
503
+ [[package]]
504
+ name = "codex-file-search"
505
+ -version = "0.0.0"
506
+ +version = "0.154.0"
507
+ dependencies = [
508
+ "anyhow",
509
+ "clap",
510
+ @@ -3452,7 +3452,7 @@ dependencies = [
511
+
512
+ [[package]]
513
+ name = "codex-file-system"
514
+ -version = "0.0.0"
515
+ +version = "0.154.0"
516
+ dependencies = [
517
+ "bytes",
518
+ "codex-protocol",
519
+ @@ -3464,7 +3464,7 @@ dependencies = [
520
+
521
+ [[package]]
522
+ name = "codex-file-watcher"
523
+ -version = "0.0.0"
524
+ +version = "0.154.0"
525
+ dependencies = [
526
+ "notify",
527
+ "pretty_assertions",
528
+ @@ -3475,7 +3475,7 @@ dependencies = [
529
+
530
+ [[package]]
531
+ name = "codex-git-attribution"
532
+ -version = "0.0.0"
533
+ +version = "0.154.0"
534
+ dependencies = [
535
+ "codex-backend-client",
536
+ "codex-extension-api",
537
+ @@ -3488,7 +3488,7 @@ dependencies = [
538
+
539
+ [[package]]
540
+ name = "codex-git-utils"
541
+ -version = "0.0.0"
542
+ +version = "0.154.0"
543
+ dependencies = [
544
+ "anyhow",
545
+ "chrono",
546
+ @@ -3514,7 +3514,7 @@ dependencies = [
547
+
548
+ [[package]]
549
+ name = "codex-goal-extension"
550
+ -version = "0.0.0"
551
+ +version = "0.154.0"
552
+ dependencies = [
553
+ "anyhow",
554
+ "chrono",
555
+ @@ -3538,7 +3538,7 @@ dependencies = [
556
+
557
+ [[package]]
558
+ name = "codex-guardian-context"
559
+ -version = "0.0.0"
560
+ +version = "0.154.0"
561
+ dependencies = [
562
+ "codex-history",
563
+ "codex-protocol",
564
+ @@ -3548,7 +3548,7 @@ dependencies = [
565
+
566
+ [[package]]
567
+ name = "codex-guardian-v2"
568
+ -version = "0.0.0"
569
+ +version = "0.154.0"
570
+ dependencies = [
571
+ "anyhow",
572
+ "codex-analytics",
573
+ @@ -3578,7 +3578,7 @@ dependencies = [
574
+
575
+ [[package]]
576
+ name = "codex-history"
577
+ -version = "0.0.0"
578
+ +version = "0.154.0"
579
+ dependencies = [
580
+ "anyhow",
581
+ "codex-protocol",
582
+ @@ -3590,7 +3590,7 @@ dependencies = [
583
+
584
+ [[package]]
585
+ name = "codex-history-notes-extension"
586
+ -version = "0.0.0"
587
+ +version = "0.154.0"
588
+ dependencies = [
589
+ "codex-analytics",
590
+ "codex-api",
591
+ @@ -3615,7 +3615,7 @@ dependencies = [
592
+
593
+ [[package]]
594
+ name = "codex-home"
595
+ -version = "0.0.0"
596
+ +version = "0.154.0"
597
+ dependencies = [
598
+ "codex-extension-api",
599
+ "codex-utils-absolute-path",
600
+ @@ -3626,7 +3626,7 @@ dependencies = [
601
+
602
+ [[package]]
603
+ name = "codex-hooks"
604
+ -version = "0.0.0"
605
+ +version = "0.154.0"
606
+ dependencies = [
607
+ "anyhow",
608
+ "async-channel",
609
+ @@ -3652,7 +3652,7 @@ dependencies = [
610
+
611
+ [[package]]
612
+ name = "codex-http-client"
613
+ -version = "0.0.0"
614
+ +version = "0.154.0"
615
+ dependencies = [
616
+ "bytes",
617
+ "codex-utils-cargo-bin",
618
+ @@ -3684,7 +3684,7 @@ dependencies = [
619
+
620
+ [[package]]
621
+ name = "codex-image-generation-extension"
622
+ -version = "0.0.0"
623
+ +version = "0.154.0"
624
+ dependencies = [
625
+ "base64 0.22.1",
626
+ "codex-api",
627
+ @@ -3711,7 +3711,7 @@ dependencies = [
628
+
629
+ [[package]]
630
+ name = "codex-install-context"
631
+ -version = "0.0.0"
632
+ +version = "0.154.0"
633
+ dependencies = [
634
+ "codex-utils-absolute-path",
635
+ "codex-utils-home-dir",
636
+ @@ -3724,7 +3724,7 @@ dependencies = [
637
+
638
+ [[package]]
639
+ name = "codex-keyring-store"
640
+ -version = "0.0.0"
641
+ +version = "0.154.0"
642
+ dependencies = [
643
+ "keyring",
644
+ "tracing",
645
+ @@ -3732,7 +3732,7 @@ dependencies = [
646
+
647
+ [[package]]
648
+ name = "codex-linux-sandbox"
649
+ -version = "0.0.0"
650
+ +version = "0.154.0"
651
+ dependencies = [
652
+ "clap",
653
+ "codex-core",
654
+ @@ -3759,7 +3759,7 @@ dependencies = [
655
+
656
+ [[package]]
657
+ name = "codex-lmstudio"
658
+ -version = "0.0.0"
659
+ +version = "0.154.0"
660
+ dependencies = [
661
+ "codex-core",
662
+ "codex-http-client",
663
+ @@ -3773,7 +3773,7 @@ dependencies = [
664
+
665
+ [[package]]
666
+ name = "codex-login"
667
+ -version = "0.0.0"
668
+ +version = "0.154.0"
669
+ dependencies = [
670
+ "anyhow",
671
+ "base64 0.22.1",
672
+ @@ -3816,7 +3816,7 @@ dependencies = [
673
+
674
+ [[package]]
675
+ name = "codex-mcp"
676
+ -version = "0.0.0"
677
+ +version = "0.154.0"
678
+ dependencies = [
679
+ "anyhow",
680
+ "arc-swap",
681
+ @@ -3856,7 +3856,7 @@ dependencies = [
682
+
683
+ [[package]]
684
+ name = "codex-mcp-extension"
685
+ -version = "0.0.0"
686
+ +version = "0.154.0"
687
+ dependencies = [
688
+ "anyhow",
689
+ "axum",
690
+ @@ -3887,7 +3887,7 @@ dependencies = [
691
+
692
+ [[package]]
693
+ name = "codex-memories-extension"
694
+ -version = "0.0.0"
695
+ +version = "0.154.0"
696
+ dependencies = [
697
+ "codex-core",
698
+ "codex-extension-api",
699
+ @@ -3908,7 +3908,7 @@ dependencies = [
700
+
701
+ [[package]]
702
+ name = "codex-memories-read"
703
+ -version = "0.0.0"
704
+ +version = "0.154.0"
705
+ dependencies = [
706
+ "codex-protocol",
707
+ "codex-shell-command",
708
+ @@ -3918,7 +3918,7 @@ dependencies = [
709
+
710
+ [[package]]
711
+ name = "codex-memories-write"
712
+ -version = "0.0.0"
713
+ +version = "0.154.0"
714
+ dependencies = [
715
+ "anyhow",
716
+ "chrono",
717
+ @@ -3955,7 +3955,7 @@ dependencies = [
718
+
719
+ [[package]]
720
+ name = "codex-message-history"
721
+ -version = "0.0.0"
722
+ +version = "0.154.0"
723
+ dependencies = [
724
+ "codex-config",
725
+ "memchr",
726
+ @@ -3969,7 +3969,7 @@ dependencies = [
727
+
728
+ [[package]]
729
+ name = "codex-model-provider"
730
+ -version = "0.0.0"
731
+ +version = "0.154.0"
732
+ dependencies = [
733
+ "codex-agent-identity",
734
+ "codex-api",
735
+ @@ -3993,7 +3993,7 @@ dependencies = [
736
+
737
+ [[package]]
738
+ name = "codex-model-provider-info"
739
+ -version = "0.0.0"
740
+ +version = "0.154.0"
741
+ dependencies = [
742
+ "codex-api",
743
+ "codex-protocol",
744
+ @@ -4010,7 +4010,7 @@ dependencies = [
745
+
746
+ [[package]]
747
+ name = "codex-models-manager"
748
+ -version = "0.0.0"
749
+ +version = "0.154.0"
750
+ dependencies = [
751
+ "chrono",
752
+ "codex-collaboration-mode-templates",
753
+ @@ -4029,7 +4029,7 @@ dependencies = [
754
+
755
+ [[package]]
756
+ name = "codex-mxc-sandbox"
757
+ -version = "0.0.0"
758
+ +version = "0.154.0"
759
+ dependencies = [
760
+ "anyhow",
761
+ "appcontainer_common",
762
+ @@ -4041,7 +4041,7 @@ dependencies = [
763
+
764
+ [[package]]
765
+ name = "codex-network-proxy"
766
+ -version = "0.0.0"
767
+ +version = "0.154.0"
768
+ dependencies = [
769
+ "anyhow",
770
+ "base64 0.22.1",
771
+ @@ -4080,7 +4080,7 @@ dependencies = [
772
+
773
+ [[package]]
774
+ name = "codex-ollama"
775
+ -version = "0.0.0"
776
+ +version = "0.154.0"
777
+ dependencies = [
778
+ "assert_matches",
779
+ "async-stream",
780
+ @@ -4100,7 +4100,7 @@ dependencies = [
781
+
782
+ [[package]]
783
+ name = "codex-otel"
784
+ -version = "0.0.0"
785
+ +version = "0.154.0"
786
+ dependencies = [
787
+ "chrono",
788
+ "codex-api",
789
+ @@ -4133,7 +4133,7 @@ dependencies = [
790
+
791
+ [[package]]
792
+ name = "codex-otel-trace-websocket"
793
+ -version = "0.0.0"
794
+ +version = "0.154.0"
795
+ dependencies = [
796
+ "anyhow",
797
+ "axum",
798
+ @@ -4144,7 +4144,7 @@ dependencies = [
799
+
800
+ [[package]]
801
+ name = "codex-plugin"
802
+ -version = "0.0.0"
803
+ +version = "0.154.0"
804
+ dependencies = [
805
+ "codex-config",
806
+ "codex-protocol",
807
+ @@ -4158,7 +4158,7 @@ dependencies = [
808
+
809
+ [[package]]
810
+ name = "codex-process-hardening"
811
+ -version = "0.0.0"
812
+ +version = "0.154.0"
813
+ dependencies = [
814
+ "libc",
815
+ "pretty_assertions",
816
+ @@ -4166,7 +4166,7 @@ dependencies = [
817
+
818
+ [[package]]
819
+ name = "codex-prompts"
820
+ -version = "0.0.0"
821
+ +version = "0.154.0"
822
+ dependencies = [
823
+ "anyhow",
824
+ "codex-context-fragments",
825
+ @@ -4180,7 +4180,7 @@ dependencies = [
826
+
827
+ [[package]]
828
+ name = "codex-protocol"
829
+ -version = "0.0.0"
830
+ +version = "0.154.0"
831
+ dependencies = [
832
+ "anyhow",
833
+ "chardetng",
834
+ @@ -4224,7 +4224,7 @@ dependencies = [
835
+
836
+ [[package]]
837
+ name = "codex-queue-extension"
838
+ -version = "0.0.0"
839
+ +version = "0.154.0"
840
+ dependencies = [
841
+ "anyhow",
842
+ "codex-core",
843
+ @@ -4245,7 +4245,7 @@ dependencies = [
844
+
845
+ [[package]]
846
+ name = "codex-realtime-webrtc"
847
+ -version = "0.0.0"
848
+ +version = "0.154.0"
849
+ dependencies = [
850
+ "anyhow",
851
+ "codex-build-info",
852
+ @@ -4262,7 +4262,7 @@ dependencies = [
853
+
854
+ [[package]]
855
+ name = "codex-response-debug-context"
856
+ -version = "0.0.0"
857
+ +version = "0.154.0"
858
+ dependencies = [
859
+ "base64 0.22.1",
860
+ "codex-api",
861
+ @@ -4273,7 +4273,7 @@ dependencies = [
862
+
863
+ [[package]]
864
+ name = "codex-responses-api-proxy"
865
+ -version = "0.0.0"
866
+ +version = "0.154.0"
867
+ dependencies = [
868
+ "anyhow",
869
+ "clap",
870
+ @@ -4290,7 +4290,7 @@ dependencies = [
871
+
872
+ [[package]]
873
+ name = "codex-rmcp-client"
874
+ -version = "0.0.0"
875
+ +version = "0.154.0"
876
+ dependencies = [
877
+ "anyhow",
878
+ "axum",
879
+ @@ -4336,7 +4336,7 @@ dependencies = [
880
+
881
+ [[package]]
882
+ name = "codex-rollout"
883
+ -version = "0.0.0"
884
+ +version = "0.154.0"
885
+ dependencies = [
886
+ "anyhow",
887
+ "chrono",
888
+ @@ -4363,7 +4363,7 @@ dependencies = [
889
+
890
+ [[package]]
891
+ name = "codex-rollout-trace"
892
+ -version = "0.0.0"
893
+ +version = "0.154.0"
894
+ dependencies = [
895
+ "anyhow",
896
+ "codex-code-mode",
897
+ @@ -4379,7 +4379,7 @@ dependencies = [
898
+
899
+ [[package]]
900
+ name = "codex-sandboxing"
901
+ -version = "0.0.0"
902
+ +version = "0.154.0"
903
+ dependencies = [
904
+ "anyhow",
905
+ "codex-mxc-sandbox",
906
+ @@ -4405,7 +4405,7 @@ dependencies = [
907
+
908
+ [[package]]
909
+ name = "codex-secrets"
910
+ -version = "0.0.0"
911
+ +version = "0.154.0"
912
+ dependencies = [
913
+ "age",
914
+ "anyhow",
915
+ @@ -4426,7 +4426,7 @@ dependencies = [
916
+
917
+ [[package]]
918
+ name = "codex-shell-command"
919
+ -version = "0.0.0"
920
+ +version = "0.154.0"
921
+ dependencies = [
922
+ "anyhow",
923
+ "base64 0.22.1",
924
+ @@ -4449,7 +4449,7 @@ dependencies = [
925
+
926
+ [[package]]
927
+ name = "codex-shell-escalation"
928
+ -version = "0.0.0"
929
+ +version = "0.154.0"
930
+ dependencies = [
931
+ "anyhow",
932
+ "clap",
933
+ @@ -4469,7 +4469,7 @@ dependencies = [
934
+
935
+ [[package]]
936
+ name = "codex-skills"
937
+ -version = "0.0.0"
938
+ +version = "0.154.0"
939
+ dependencies = [
940
+ "codex-protocol",
941
+ "codex-shell-command",
942
+ @@ -4486,7 +4486,7 @@ dependencies = [
943
+
944
+ [[package]]
945
+ name = "codex-skills-extension"
946
+ -version = "0.0.0"
947
+ +version = "0.154.0"
948
+ dependencies = [
949
+ "assert_matches",
950
+ "codex-analytics",
951
+ @@ -4523,7 +4523,7 @@ dependencies = [
952
+
953
+ [[package]]
954
+ name = "codex-state"
955
+ -version = "0.0.0"
956
+ +version = "0.154.0"
957
+ dependencies = [
958
+ "anyhow",
959
+ "chrono",
960
+ @@ -4547,7 +4547,7 @@ dependencies = [
961
+
962
+ [[package]]
963
+ name = "codex-stdio-to-uds"
964
+ -version = "0.0.0"
965
+ +version = "0.154.0"
966
+ dependencies = [
967
+ "anyhow",
968
+ "codex-uds",
969
+ @@ -4559,7 +4559,7 @@ dependencies = [
970
+
971
+ [[package]]
972
+ name = "codex-terminal-detection"
973
+ -version = "0.0.0"
974
+ +version = "0.154.0"
975
+ dependencies = [
976
+ "pretty_assertions",
977
+ "tracing",
978
+ @@ -4567,7 +4567,7 @@ dependencies = [
979
+
980
+ [[package]]
981
+ name = "codex-test-binary-support"
982
+ -version = "0.0.0"
983
+ +version = "0.154.0"
984
+ dependencies = [
985
+ "codex-arg0",
986
+ "tempfile",
987
+ @@ -4575,7 +4575,7 @@ dependencies = [
988
+
989
+ [[package]]
990
+ name = "codex-thread-manager-sample"
991
+ -version = "0.0.0"
992
+ +version = "0.154.0"
993
+ dependencies = [
994
+ "anyhow",
995
+ "clap",
996
+ @@ -4586,7 +4586,7 @@ dependencies = [
997
+
998
+ [[package]]
999
+ name = "codex-thread-store"
1000
+ -version = "0.0.0"
1001
+ +version = "0.154.0"
1002
+ dependencies = [
1003
+ "chrono",
1004
+ "codex-app-server-protocol",
1005
+ @@ -4616,7 +4616,7 @@ dependencies = [
1006
+
1007
+ [[package]]
1008
+ name = "codex-tools"
1009
+ -version = "0.0.0"
1010
+ +version = "0.154.0"
1011
+ dependencies = [
1012
+ "bitflags 2.13.1",
1013
+ "codex-code-mode",
1014
+ @@ -4641,7 +4641,7 @@ dependencies = [
1015
+
1016
+ [[package]]
1017
+ name = "codex-tui"
1018
+ -version = "0.0.0"
1019
+ +version = "0.154.0"
1020
+ dependencies = [
1021
+ "anyhow",
1022
+ "app_test_support",
1023
+ @@ -4758,7 +4758,7 @@ dependencies = [
1024
+
1025
+ [[package]]
1026
+ name = "codex-uds"
1027
+ -version = "0.0.0"
1028
+ +version = "0.154.0"
1029
+ dependencies = [
1030
+ "async-io",
1031
+ "pretty_assertions",
1032
+ @@ -4771,7 +4771,7 @@ dependencies = [
1033
+
1034
+ [[package]]
1035
+ name = "codex-utils-absolute-path"
1036
+ -version = "0.0.0"
1037
+ +version = "0.154.0"
1038
+ dependencies = [
1039
+ "dirs",
1040
+ "dunce",
1041
+ @@ -4785,14 +4785,14 @@ dependencies = [
1042
+
1043
+ [[package]]
1044
+ name = "codex-utils-approval-presets"
1045
+ -version = "0.0.0"
1046
+ +version = "0.154.0"
1047
+ dependencies = [
1048
+ "codex-protocol",
1049
+ ]
1050
+
1051
+ [[package]]
1052
+ name = "codex-utils-audio"
1053
+ -version = "0.0.0"
1054
+ +version = "0.154.0"
1055
+ dependencies = [
1056
+ "base64 0.22.1",
1057
+ "codex-protocol",
1058
+ @@ -4806,7 +4806,7 @@ dependencies = [
1059
+
1060
+ [[package]]
1061
+ name = "codex-utils-cache"
1062
+ -version = "0.0.0"
1063
+ +version = "0.154.0"
1064
+ dependencies = [
1065
+ "lru",
1066
+ "sha1 0.10.6",
1067
+ @@ -4815,7 +4815,7 @@ dependencies = [
1068
+
1069
+ [[package]]
1070
+ name = "codex-utils-cargo-bin"
1071
+ -version = "0.0.0"
1072
+ +version = "0.154.0"
1073
+ dependencies = [
1074
+ "assert_cmd",
1075
+ "runfiles",
1076
+ @@ -4824,7 +4824,7 @@ dependencies = [
1077
+
1078
+ [[package]]
1079
+ name = "codex-utils-cli"
1080
+ -version = "0.0.0"
1081
+ +version = "0.154.0"
1082
+ dependencies = [
1083
+ "clap",
1084
+ "codex-protocol",
1085
+ @@ -4836,15 +4836,15 @@ dependencies = [
1086
+
1087
+ [[package]]
1088
+ name = "codex-utils-elapsed"
1089
+ -version = "0.0.0"
1090
+ +version = "0.154.0"
1091
+
1092
+ [[package]]
1093
+ name = "codex-utils-fuzzy-match"
1094
+ -version = "0.0.0"
1095
+ +version = "0.154.0"
1096
+
1097
+ [[package]]
1098
+ name = "codex-utils-git-discovery"
1099
+ -version = "0.0.0"
1100
+ +version = "0.154.0"
1101
+ dependencies = [
1102
+ "codex-git-utils",
1103
+ "codex-utils-absolute-path",
1104
+ @@ -4857,7 +4857,7 @@ dependencies = [
1105
+
1106
+ [[package]]
1107
+ name = "codex-utils-home-dir"
1108
+ -version = "0.0.0"
1109
+ +version = "0.154.0"
1110
+ dependencies = [
1111
+ "codex-utils-absolute-path",
1112
+ "dirs",
1113
+ @@ -4867,7 +4867,7 @@ dependencies = [
1114
+
1115
+ [[package]]
1116
+ name = "codex-utils-image"
1117
+ -version = "0.0.0"
1118
+ +version = "0.154.0"
1119
+ dependencies = [
1120
+ "base64 0.22.1",
1121
+ "codex-utils-cache",
1122
+ @@ -4880,7 +4880,7 @@ dependencies = [
1123
+
1124
+ [[package]]
1125
+ name = "codex-utils-json-to-toml"
1126
+ -version = "0.0.0"
1127
+ +version = "0.154.0"
1128
+ dependencies = [
1129
+ "pretty_assertions",
1130
+ "serde_json",
1131
+ @@ -4889,7 +4889,7 @@ dependencies = [
1132
+
1133
+ [[package]]
1134
+ name = "codex-utils-oss"
1135
+ -version = "0.0.0"
1136
+ +version = "0.154.0"
1137
+ dependencies = [
1138
+ "codex-core",
1139
+ "codex-lmstudio",
1140
+ @@ -4899,7 +4899,7 @@ dependencies = [
1141
+
1142
+ [[package]]
1143
+ name = "codex-utils-output-truncation"
1144
+ -version = "0.0.0"
1145
+ +version = "0.154.0"
1146
+ dependencies = [
1147
+ "codex-protocol",
1148
+ "codex-utils-string",
1149
+ @@ -4908,7 +4908,7 @@ dependencies = [
1150
+
1151
+ [[package]]
1152
+ name = "codex-utils-path"
1153
+ -version = "0.0.0"
1154
+ +version = "0.154.0"
1155
+ dependencies = [
1156
+ "codex-utils-absolute-path",
1157
+ "dunce",
1158
+ @@ -4919,7 +4919,7 @@ dependencies = [
1159
+
1160
+ [[package]]
1161
+ name = "codex-utils-path-uri"
1162
+ -version = "0.0.0"
1163
+ +version = "0.154.0"
1164
+ dependencies = [
1165
+ "base64 0.22.1",
1166
+ "codex-utils-absolute-path",
1167
+ @@ -4935,7 +4935,7 @@ dependencies = [
1168
+
1169
+ [[package]]
1170
+ name = "codex-utils-plugins"
1171
+ -version = "0.0.0"
1172
+ +version = "0.154.0"
1173
+ dependencies = [
1174
+ "codex-exec-server",
1175
+ "codex-exec-server-protocol",
1176
+ @@ -4949,7 +4949,7 @@ dependencies = [
1177
+
1178
+ [[package]]
1179
+ name = "codex-utils-pty"
1180
+ -version = "0.0.0"
1181
+ +version = "0.154.0"
1182
+ dependencies = [
1183
+ "anyhow",
1184
+ "filedescriptor",
1185
+ @@ -4965,7 +4965,7 @@ dependencies = [
1186
+
1187
+ [[package]]
1188
+ name = "codex-utils-readiness"
1189
+ -version = "0.0.0"
1190
+ +version = "0.154.0"
1191
+ dependencies = [
1192
+ "assert_matches",
1193
+ "thiserror 2.0.18",
1194
+ @@ -4975,7 +4975,7 @@ dependencies = [
1195
+
1196
+ [[package]]
1197
+ name = "codex-utils-redacted-string"
1198
+ -version = "0.0.0"
1199
+ +version = "0.154.0"
1200
+ dependencies = [
1201
+ "schemars 0.8.22",
1202
+ "serde",
1203
+ @@ -4983,14 +4983,14 @@ dependencies = [
1204
+
1205
+ [[package]]
1206
+ name = "codex-utils-rustls-provider"
1207
+ -version = "0.0.0"
1208
+ +version = "0.154.0"
1209
+ dependencies = [
1210
+ "rustls",
1211
+ ]
1212
+
1213
+ [[package]]
1214
+ name = "codex-utils-sandbox-summary"
1215
+ -version = "0.0.0"
1216
+ +version = "0.154.0"
1217
+ dependencies = [
1218
+ "codex-protocol",
1219
+ "codex-utils-absolute-path",
1220
+ @@ -4999,7 +4999,7 @@ dependencies = [
1221
+
1222
+ [[package]]
1223
+ name = "codex-utils-sleep-inhibitor"
1224
+ -version = "0.0.0"
1225
+ +version = "0.154.0"
1226
+ dependencies = [
1227
+ "core-foundation 0.9.4",
1228
+ "libc",
1229
+ @@ -5009,14 +5009,14 @@ dependencies = [
1230
+
1231
+ [[package]]
1232
+ name = "codex-utils-stream-parser"
1233
+ -version = "0.0.0"
1234
+ +version = "0.154.0"
1235
+ dependencies = [
1236
+ "pretty_assertions",
1237
+ ]
1238
+
1239
+ [[package]]
1240
+ name = "codex-utils-string"
1241
+ -version = "0.0.0"
1242
+ +version = "0.154.0"
1243
+ dependencies = [
1244
+ "pretty_assertions",
1245
+ "regex-lite",
1246
+ @@ -5026,14 +5026,14 @@ dependencies = [
1247
+
1248
+ [[package]]
1249
+ name = "codex-utils-template"
1250
+ -version = "0.0.0"
1251
+ +version = "0.154.0"
1252
+ dependencies = [
1253
+ "pretty_assertions",
1254
+ ]
1255
+
1256
+ [[package]]
1257
+ name = "codex-v8-poc"
1258
+ -version = "0.0.0"
1259
+ +version = "0.154.0"
1260
+ dependencies = [
1261
+ "pretty_assertions",
1262
+ "v8",
1263
+ @@ -5041,7 +5041,7 @@ dependencies = [
1264
+
1265
+ [[package]]
1266
+ name = "codex-voice-host"
1267
+ -version = "0.0.0"
1268
+ +version = "0.154.0"
1269
+ dependencies = [
1270
+ "anyhow",
1271
+ "codex-install-context",
1272
+ @@ -5068,7 +5068,7 @@ dependencies = [
1273
+
1274
+ [[package]]
1275
+ name = "codex-web-search-extension"
1276
+ -version = "0.0.0"
1277
+ +version = "0.154.0"
1278
+ dependencies = [
1279
+ "codex-api",
1280
+ "codex-core",
1281
+ @@ -5089,7 +5089,7 @@ dependencies = [
1282
+
1283
+ [[package]]
1284
+ name = "codex-websocket-client"
1285
+ -version = "0.0.0"
1286
+ +version = "0.154.0"
1287
+ dependencies = [
1288
+ "codex-http-client",
1289
+ "codex-utils-rustls-provider",
1290
+ @@ -5105,7 +5105,7 @@ dependencies = [
1291
+
1292
+ [[package]]
1293
+ name = "codex-windows-sandbox"
1294
+ -version = "0.0.0"
1295
+ +version = "0.154.0"
1296
+ dependencies = [
1297
+ "anyhow",
1298
+ "base64 0.22.1",
1299
+ @@ -5131,7 +5131,7 @@ dependencies = [
1300
+
1301
+ [[package]]
1302
+ name = "codex-windows-sandbox-service"
1303
+ -version = "0.0.0"
1304
+ +version = "0.154.0"
1305
+ dependencies = [
1306
+ "anyhow",
1307
+ "codex-cloud-config",
1308
+ @@ -5148,7 +5148,7 @@ dependencies = [
1309
+
1310
+ [[package]]
1311
+ name = "codex-workload-identity"
1312
+ -version = "0.0.0"
1313
+ +version = "0.154.0"
1314
+ dependencies = [
1315
+ "codex-http-client",
1316
+ "pretty_assertions",
1317
+ @@ -5163,7 +5163,7 @@ dependencies = [
1318
+
1319
+ [[package]]
1320
+ name = "codex-worktree"
1321
+ -version = "0.0.0"
1322
+ +version = "0.154.0"
1323
+ dependencies = [
1324
+ "anyhow",
1325
+ "codex-git-utils",
1326
+ @@ -5403,7 +5403,7 @@ dependencies = [
1327
+
1328
+ [[package]]
1329
+ name = "core_test_support"
1330
+ -version = "0.0.0"
1331
+ +version = "0.154.0"
1332
+ dependencies = [
1333
+ "anyhow",
1334
+ "assert_cmd",
1335
+ diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs
1336
+ index b48aa9c01b..940a82d78b 100644
1337
+ --- a/codex-rs/tui/src/app/background_requests.rs
1338
+ +++ b/codex-rs/tui/src/app/background_requests.rs
1339
+ @@ -103,7 +103,8 @@ impl App {
1340
+ RateLimitRefreshOrigin::Recovery
1341
+ | RateLimitRefreshOrigin::Periodic
1342
+ | RateLimitRefreshOrigin::ResetConsume { .. }
1343
+ - | RateLimitRefreshOrigin::ResetPicker { .. } => {
1344
+ + | RateLimitRefreshOrigin::ResetPicker { .. }
1345
+ + | RateLimitRefreshOrigin::CxStatusLine => {
1346
+ tokio::time::timeout(RATE_LIMIT_RESET_REQUEST_TIMEOUT, request)
1347
+ .await
1348
+ .map_err(|_| "account/rateLimits/read timed out in TUI".to_string())
1349
+ @@ -1373,7 +1374,75 @@ mod tests {
1350
+ use codex_config::types::AuthCredentialsStoreMode;
1351
+ use codex_protocol::mcp::Tool;
1352
+ use codex_utils_absolute_path::AbsolutePathBuf;
1353
+ + use codex_utils_absolute_path::test_support::PathExt;
1354
+ use pretty_assertions::assert_eq;
1355
+ + use tempfile::tempdir;
1356
+ + use wiremock::Mock;
1357
+ + use wiremock::MockServer;
1358
+ + use wiremock::ResponseTemplate;
1359
+ + use wiremock::matchers::method;
1360
+ + use wiremock::matchers::path;
1361
+ +
1362
+ + #[tokio::test(flavor = "current_thread")]
1363
+ + async fn cxstatusline_rate_limit_request_times_out() -> Result<()> {
1364
+ + let mut app = make_test_app().await;
1365
+ + let server = MockServer::start().await;
1366
+ + let home = tempdir()?;
1367
+ + app.config.codex_home = home.path().to_path_buf().abs();
1368
+ + app.config.chatgpt_base_url = server.uri();
1369
+ + app.config.cli_auth_credentials_store_mode = AuthCredentialsStoreMode::File;
1370
+ + app.config.sqlite = codex_state::SqliteConfig::new_for_testing(home.path().abs());
1371
+ + write_chatgpt_auth(
1372
+ + home.path(),
1373
+ + ChatGptAuthFixture::new("chatgpt-token").account_id("account-123"),
1374
+ + AuthCredentialsStoreMode::File,
1375
+ + )
1376
+ + .expect("write ChatGPT authentication");
1377
+ + Mock::given(method("GET"))
1378
+ + .and(path("/api/codex/usage"))
1379
+ + .respond_with(ResponseTemplate::new(/*status_code*/ 200).set_delay(
1380
+ + RATE_LIMIT_RESET_REQUEST_TIMEOUT + std::time::Duration::from_secs(/*secs*/ 1),
1381
+ + ))
1382
+ + .expect(/*requests*/ 1)
1383
+ + .mount(&server)
1384
+ + .await;
1385
+ + let app_server = crate::start_embedded_app_server_for_picker(&app.config).await?;
1386
+ + let (app_event_tx, mut app_events) = tokio::sync::mpsc::unbounded_channel();
1387
+ + app.app_event_tx = AppEventSender::new(app_event_tx);
1388
+ +
1389
+ + app.refresh_rate_limits(&app_server, RateLimitRefreshOrigin::CxStatusLine);
1390
+ + tokio::time::timeout(std::time::Duration::from_secs(/*secs*/ 5), async {
1391
+ + loop {
1392
+ + if server.received_requests().await.is_some_and(|requests| {
1393
+ + requests
1394
+ + .iter()
1395
+ + .any(|request| request.url.path() == "/api/codex/usage")
1396
+ + }) {
1397
+ + return;
1398
+ + }
1399
+ + tokio::time::sleep(std::time::Duration::from_millis(/*millis*/ 10)).await;
1400
+ + }
1401
+ + })
1402
+ + .await
1403
+ + .expect("cxstatusline request reaches the usage endpoint");
1404
+ +
1405
+ + tokio::time::pause();
1406
+ + tokio::time::advance(RATE_LIMIT_RESET_REQUEST_TIMEOUT).await;
1407
+ + tokio::task::yield_now().await;
1408
+ +
1409
+ + assert!(matches!(
1410
+ + app_events.try_recv(),
1411
+ + Ok(AppEvent::RateLimitsLoaded {
1412
+ + origin: RateLimitRefreshOrigin::CxStatusLine,
1413
+ + result: Err(ref error),
1414
+ + ..
1415
+ + }) if error == "account/rateLimits/read timed out in TUI"
1416
+ + ));
1417
+ +
1418
+ + app_server.shutdown().await?;
1419
+ + server.verify().await;
1420
+ + Ok(())
1421
+ + }
1422
+
1423
+ #[tokio::test]
1424
+ async fn fetch_thread_usage_uses_app_server_auth_after_persisted_account_changes() {
1425
+ diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs
1426
+ index cc16390c17..3d98593d72 100644
1427
+ --- a/codex-rs/tui/src/app/event_dispatch.rs
1428
+ +++ b/codex-rs/tui/src/app/event_dispatch.rs
1429
+ @@ -1439,6 +1439,13 @@ impl App {
1430
+ }),
1431
+ );
1432
+ }
1433
+ + RateLimitRefreshOrigin::CxStatusLine => {
1434
+ + self.chat_widget.finish_cxstatusline_rate_limit_refresh();
1435
+ + for snapshot in snapshots {
1436
+ + self.chat_widget.on_rate_limit_snapshot(Some(snapshot));
1437
+ + }
1438
+ + tui.frame_requester().schedule_frame();
1439
+ + }
1440
+ }
1441
+ }
1442
+ Err(err) => {
1443
+ @@ -1484,6 +1491,10 @@ impl App {
1444
+ Err(err),
1445
+ );
1446
+ }
1447
+ + RateLimitRefreshOrigin::CxStatusLine => {
1448
+ + self.chat_widget.finish_cxstatusline_rate_limit_refresh();
1449
+ + tui.frame_requester().schedule_frame();
1450
+ + }
1451
+ }
1452
+ }
1453
+ }
1454
+ diff --git a/codex-rs/tui/src/app/tests/rate_limits.rs b/codex-rs/tui/src/app/tests/rate_limits.rs
1455
+ index 344c703aea..acba1debf6 100644
1456
+ --- a/codex-rs/tui/src/app/tests/rate_limits.rs
1457
+ +++ b/codex-rs/tui/src/app/tests/rate_limits.rs
1458
+ @@ -216,6 +216,7 @@ async fn stale_rate_limit_reads_preserve_newer_workspace_hard_stop_for_every_ori
1459
+ "usage",
1460
+ "reset-picker",
1461
+ "reset-consume",
1462
+ + "cxstatusline",
1463
+ ] {
1464
+ let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
1465
+ set_chatgpt_auth(&mut app.chat_widget);
1466
+ @@ -270,6 +271,7 @@ async fn stale_rate_limit_reads_preserve_newer_workspace_hard_stop_for_every_ori
1467
+ "reset-consume" => RateLimitRefreshOrigin::ResetConsume {
1468
+ request_id: app.chat_widget.show_rate_limit_reset_consuming_popup(),
1469
+ },
1470
+ + "cxstatusline" => RateLimitRefreshOrigin::CxStatusLine,
1471
+ _ => unreachable!("unknown refresh origin"),
1472
+ };
1473
+ let read_generation = app.rate_limit_hard_stop_generation;
1474
+ @@ -310,7 +312,7 @@ async fn stale_rate_limit_reads_preserve_newer_workspace_hard_stop_for_every_ori
1475
+ "reset-consume" => {
1476
+ assert!(popup.contains("Usage reset. You have 0 usage limit resets left."));
1477
+ }
1478
+ - "startup" | "status" => {}
1479
+ + "startup" | "status" | "cxstatusline" => {}
1480
+ _ => unreachable!("unknown refresh origin"),
1481
+ }
1482
+
1483
+ @@ -486,6 +488,97 @@ async fn failed_rate_limit_read_preserves_visible_backend_banner() -> Result<()>
1484
+ Ok(())
1485
+ }
1486
+
1487
+ +#[tokio::test]
1488
+ +async fn cxstatusline_rate_limit_refresh_updates_snapshots_without_status_card() -> Result<()> {
1489
+ + let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
1490
+ + set_chatgpt_auth(&mut app.chat_widget);
1491
+ + let mut tui = crate::tui::test_support::make_test_tui()?;
1492
+ + let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(
1493
+ + app.chat_widget.config_ref(),
1494
+ + ))
1495
+ + .await?;
1496
+ + let (request_id, hard_stop_generation) = app
1497
+ + .rate_limit_refresh_state
1498
+ + .start(
1499
+ + RateLimitRefreshOrigin::CxStatusLine,
1500
+ + &mut app.rate_limit_hard_stop_generation,
1501
+ + )
1502
+ + .expect("cxstatusline refresh starts");
1503
+ +
1504
+ + app.handle_event(
1505
+ + &mut tui,
1506
+ + &mut app_server,
1507
+ + AppEvent::RateLimitsLoaded {
1508
+ + request_id,
1509
+ + origin: RateLimitRefreshOrigin::CxStatusLine,
1510
+ + hard_stop_generation,
1511
+ + result: Ok(account_rate_limits_response(rate_limit_snapshot(
1512
+ + /*used_percent*/ 89, /*rate_limit_reached_type*/ None,
1513
+ + /*spend_control_reached*/ None,
1514
+ + ))),
1515
+ + },
1516
+ + )
1517
+ + .await?;
1518
+ +
1519
+ + assert!(
1520
+ + !std::iter::from_fn(|| app_event_rx.try_recv().ok())
1521
+ + .any(|event| matches!(event, AppEvent::InsertHistoryCell(_))),
1522
+ + "cxstatusline refresh must not create a status card"
1523
+ + );
1524
+ + let status = render_status_output(&mut app, &mut app_event_rx);
1525
+ + assert!(
1526
+ + status.contains("11% left"),
1527
+ + "expected cxstatusline refresh to update cached limits, got: {status}"
1528
+ + );
1529
+ +
1530
+ + app_server.shutdown().await?;
1531
+ + Ok(())
1532
+ +}
1533
+ +
1534
+ +#[tokio::test]
1535
+ +async fn cxstatusline_rate_limit_refresh_failure_preserves_last_snapshot() -> Result<()> {
1536
+ + let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
1537
+ + set_chatgpt_auth(&mut app.chat_widget);
1538
+ + app.chat_widget
1539
+ + .on_rate_limit_snapshot(Some(rate_limit_snapshot(
1540
+ + /*used_percent*/ 89, /*rate_limit_reached_type*/ None,
1541
+ + /*spend_control_reached*/ None,
1542
+ + )));
1543
+ + let mut tui = crate::tui::test_support::make_test_tui()?;
1544
+ + let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(
1545
+ + app.chat_widget.config_ref(),
1546
+ + ))
1547
+ + .await?;
1548
+ + let (request_id, hard_stop_generation) = app
1549
+ + .rate_limit_refresh_state
1550
+ + .start(
1551
+ + RateLimitRefreshOrigin::CxStatusLine,
1552
+ + &mut app.rate_limit_hard_stop_generation,
1553
+ + )
1554
+ + .expect("cxstatusline refresh starts");
1555
+ +
1556
+ + app.handle_event(
1557
+ + &mut tui,
1558
+ + &mut app_server,
1559
+ + AppEvent::RateLimitsLoaded {
1560
+ + request_id,
1561
+ + origin: RateLimitRefreshOrigin::CxStatusLine,
1562
+ + hard_stop_generation,
1563
+ + result: Err("offline".to_string()),
1564
+ + },
1565
+ + )
1566
+ + .await?;
1567
+ +
1568
+ + let status = render_status_output(&mut app, &mut app_event_rx);
1569
+ + assert!(
1570
+ + status.contains("11% left"),
1571
+ + "expected failed cxstatusline refresh to preserve cached limits, got: {status}"
1572
+ + );
1573
+ +
1574
+ + app_server.shutdown().await?;
1575
+ + Ok(())
1576
+ +}
1577
+ +
1578
+ #[tokio::test]
1579
+ async fn backend_banner_reads_ignore_older_completions() -> Result<()> {
1580
+ let (mut app, _events, _ops) = make_test_app_with_channels().await;
1581
+ diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs
1582
+ index 1df5a1a7f4..72011bf065 100644
1583
+ --- a/codex-rs/tui/src/app_event.rs
1584
+ +++ b/codex-rs/tui/src/app_event.rs
1585
+ @@ -196,6 +196,8 @@ pub(crate) struct PluginRemoteSectionError {
1586
+ /// zero reset count so the disabled menu entry can become available without a
1587
+ /// restart. A `ResetPicker` refreshes the rate limits and detailed reset-credit
1588
+ /// rows before showing redemption choices.
1589
+ +/// A `CxStatusLine` refresh updates the renderer payload without creating a
1590
+ +/// status card or changing reset-credit state.
1591
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1592
+ pub(crate) enum RateLimitRefreshOrigin {
1593
+ /// Eagerly fetched after bootstrap for `/status` data and reset availability.
1594
+ @@ -213,6 +215,8 @@ pub(crate) enum RateLimitRefreshOrigin {
1595
+ Recovery,
1596
+ /// Background account usage read, scheduled more frequently near exhaustion.
1597
+ Periodic,
1598
+ + /// Periodically refresh the `CXSTATUSLINE_COMMAND` renderer payload.
1599
+ + CxStatusLine,
1600
+ }
1601
+
1602
+ #[derive(Debug, Clone, PartialEq, Eq)]
1603
+ diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs
1604
+ index 967c712fe9..8e2e179c6a 100644
1605
+ --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs
1606
+ +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs
1607
+ @@ -263,6 +263,7 @@ use super::footer::inset_footer_hint_area;
1608
+ use super::footer::max_left_width_for_right;
1609
+ use super::footer::passive_footer_status_line;
1610
+ use super::footer::render_context_right;
1611
+ +use super::footer::render_cxstatusline_block;
1612
+ use super::footer::render_footer_from_props;
1613
+ use super::footer::render_footer_hint_items;
1614
+ use super::footer::render_footer_line;
1615
+ @@ -668,6 +669,7 @@ impl ChatComposer {
1616
+ status_line_value: None,
1617
+ status_line_hyperlink_url: None,
1618
+ status_line_enabled: false,
1619
+ + cxstatusline_block: Vec::new(),
1620
+ side_conversation_context_label: None,
1621
+ active_agent_label: None,
1622
+ external_editor_key: default_keymap
1623
+ @@ -1030,9 +1032,7 @@ impl ChatComposer {
1624
+ textarea_right_reserve: u16,
1625
+ ) -> [Rect; 4] {
1626
+ let footer_props = self.footer_props();
1627
+ - let footer_hint_height = self
1628
+ - .custom_footer_height()
1629
+ - .unwrap_or_else(|| footer_height(&footer_props));
1630
+ + let footer_hint_height = self.footer_hint_height(&footer_props);
1631
+ let footer_spacing = Self::footer_spacing(footer_hint_height);
1632
+ let footer_total_height = footer_hint_height + footer_spacing;
1633
+ let popup_constraint = match &self.popups.active {
1634
+ @@ -3905,6 +3905,7 @@ impl ChatComposer {
1635
+ is_wsl,
1636
+ status_line_value: self.footer.status_line_value.clone(),
1637
+ status_line_enabled: self.footer.status_line_enabled,
1638
+ + cxstatusline_block: self.footer.cxstatusline_block.clone(),
1639
+ key_hints: FooterKeyHints {
1640
+ agents: self
1641
+ .agents_navigation_available()
1642
+ @@ -3967,6 +3968,12 @@ impl ChatComposer {
1643
+ .map(|items| if items.is_empty() { 0 } else { 1 })
1644
+ }
1645
+
1646
+ + fn footer_hint_height(&self, props: &FooterProps) -> u16 {
1647
+ + self.custom_footer_height()
1648
+ + .map(|height| height.saturating_add(props.cxstatusline_block.len() as u16))
1649
+ + .unwrap_or_else(|| footer_height(props))
1650
+ + }
1651
+ +
1652
+ pub(crate) fn sync_popups(&mut self) {
1653
+ self.sync_slash_command_elements();
1654
+ if self.history_search.is_some() || self.draft.textarea.vim_query().is_some() {
1655
+ @@ -4461,6 +4468,14 @@ impl ChatComposer {
1656
+ true
1657
+ }
1658
+
1659
+ + pub(crate) fn set_cxstatusline_block(&mut self, block: Vec<Line<'static>>) -> bool {
1660
+ + if self.footer.cxstatusline_block == block {
1661
+ + return false;
1662
+ + }
1663
+ + self.footer.cxstatusline_block = block;
1664
+ + true
1665
+ + }
1666
+ +
1667
+ pub(crate) fn set_side_conversation_context_label(&mut self, label: Option<String>) -> bool {
1668
+ if self.footer.side_conversation_context_label == label {
1669
+ return false;
1670
+ @@ -4637,9 +4652,7 @@ impl ChatComposer {
1671
+ textarea_right_reserve: u16,
1672
+ ) -> u16 {
1673
+ let footer_props = self.footer_props();
1674
+ - let footer_hint_height = self
1675
+ - .custom_footer_height()
1676
+ - .unwrap_or_else(|| footer_height(&footer_props));
1677
+ + let footer_hint_height = self.footer_hint_height(&footer_props);
1678
+ let footer_spacing = Self::footer_spacing(footer_hint_height);
1679
+ let footer_total_height = footer_hint_height + footer_spacing;
1680
+ const COLS_WITH_MARGIN: u16 = LIVE_PREFIX_COLS + 1;
1681
+ @@ -4715,9 +4728,7 @@ impl ChatComposer {
1682
+ | FooterMode::ShortcutOverlay
1683
+ | FooterMode::EscHint => false,
1684
+ };
1685
+ - let custom_height = self.custom_footer_height();
1686
+ - let footer_hint_height =
1687
+ - custom_height.unwrap_or_else(|| footer_height(&footer_props));
1688
+ + let footer_hint_height = self.footer_hint_height(&footer_props);
1689
+ let footer_spacing = Self::footer_spacing(footer_hint_height);
1690
+ let hint_rect = if footer_spacing > 0 && footer_hint_height > 0 {
1691
+ let [_, hint_rect] = Layout::vertical([
1692
+ @@ -4729,6 +4740,26 @@ impl ChatComposer {
1693
+ } else {
1694
+ popup_rect
1695
+ };
1696
+ + let cxstatusline_height =
1697
+ + (footer_props.cxstatusline_block.len() as u16).min(hint_rect.height);
1698
+ + if cxstatusline_height > 0 {
1699
+ + render_cxstatusline_block(
1700
+ + Rect::new(
1701
+ + hint_rect.x,
1702
+ + hint_rect.y,
1703
+ + hint_rect.width,
1704
+ + cxstatusline_height,
1705
+ + ),
1706
+ + buf,
1707
+ + &footer_props.cxstatusline_block,
1708
+ + );
1709
+ + }
1710
+ + let hint_rect = Rect::new(
1711
+ + hint_rect.x,
1712
+ + hint_rect.y.saturating_add(cxstatusline_height),
1713
+ + hint_rect.width,
1714
+ + hint_rect.height.saturating_sub(cxstatusline_height),
1715
+ + );
1716
+ if let Some(input) = self.draft.textarea.vim_query() {
1717
+ input.render(inset_footer_hint_area(hint_rect), buf);
1718
+ } else if let Some(line) = self.history_search_footer_line() {
1719
+ diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/footer_state.rs b/codex-rs/tui/src/bottom_pane/chat_composer/footer_state.rs
1720
+ index 009c4831f6..37fab5ba32 100644
1721
+ --- a/codex-rs/tui/src/bottom_pane/chat_composer/footer_state.rs
1722
+ +++ b/codex-rs/tui/src/bottom_pane/chat_composer/footer_state.rs
1723
+ @@ -42,6 +42,7 @@ pub(super) struct FooterState {
1724
+ pub(super) status_line_value: Option<Line<'static>>,
1725
+ pub(super) status_line_hyperlink_url: Option<String>,
1726
+ pub(super) status_line_enabled: bool,
1727
+ + pub(super) cxstatusline_block: Vec<Line<'static>>,
1728
+ pub(super) side_conversation_context_label: Option<String>,
1729
+ pub(super) active_agent_label: Option<String>,
1730
+ pub(super) external_editor_key: Option<ShortcutHint>,
1731
+ diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/history_search.rs b/codex-rs/tui/src/bottom_pane/chat_composer/history_search.rs
1732
+ index 892e8d00fd..37f9fe1204 100644
1733
+ --- a/codex-rs/tui/src/bottom_pane/chat_composer/history_search.rs
1734
+ +++ b/codex-rs/tui/src/bottom_pane/chat_composer/history_search.rs
1735
+ @@ -33,7 +33,6 @@ use ratatui::text::Span;
1736
+
1737
+ use super::super::chat_composer_history::HistorySearchDirection;
1738
+ use super::super::chat_composer_history::HistorySearchResult;
1739
+ -use super::super::footer::footer_height;
1740
+ use super::super::footer::reset_mode_after_activity;
1741
+ use super::super::textarea::VimPersistentState;
1742
+ use super::ActivePopup;
1743
+ @@ -470,9 +469,7 @@ impl ChatComposer {
1744
+ }
1745
+
1746
+ let footer_props = self.footer_props();
1747
+ - let footer_hint_height = self
1748
+ - .custom_footer_height()
1749
+ - .unwrap_or_else(|| footer_height(&footer_props));
1750
+ + let footer_hint_height = self.footer_hint_height(&footer_props);
1751
+ let footer_spacing = Self::footer_spacing(footer_hint_height);
1752
+ let hint_rect = if footer_spacing > 0 && footer_hint_height > 0 {
1753
+ let [_, hint_rect] = Layout::vertical([
1754
+ @@ -484,6 +481,16 @@ impl ChatComposer {
1755
+ } else {
1756
+ popup_rect
1757
+ };
1758
+ + let hint_rect = Rect::new(
1759
+ + hint_rect.x,
1760
+ + hint_rect
1761
+ + .y
1762
+ + .saturating_add(footer_props.cxstatusline_block.len() as u16),
1763
+ + hint_rect.width,
1764
+ + hint_rect
1765
+ + .height
1766
+ + .saturating_sub(footer_props.cxstatusline_block.len() as u16),
1767
+ + );
1768
+ if hint_rect.is_empty() {
1769
+ return None;
1770
+ }
1771
+ diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs
1772
+ index 997866be7a..ba8135bef5 100644
1773
+ --- a/codex-rs/tui/src/bottom_pane/footer.rs
1774
+ +++ b/codex-rs/tui/src/bottom_pane/footer.rs
1775
+ @@ -78,6 +78,9 @@ pub(crate) struct FooterProps {
1776
+ pub(crate) quit_shortcut_key: KeyBinding,
1777
+ pub(crate) status_line_value: Option<Line<'static>>,
1778
+ pub(crate) status_line_enabled: bool,
1779
+ + /// Complete renderer-owned rows kept separate from Codex's contextual status line so native
1780
+ + /// instructions (Esc, quit, safety) remain visible below them.
1781
+ + pub(crate) cxstatusline_block: Vec<Line<'static>>,
1782
+ pub(crate) key_hints: FooterKeyHints,
1783
+ /// Active thread label shown when the footer is rendering contextual information instead of an
1784
+ /// instructional hint.
1785
+ @@ -237,14 +240,15 @@ pub(crate) fn footer_height(props: &FooterProps) -> u16 {
1786
+ | FooterMode::ShortcutOverlay
1787
+ | FooterMode::EscHint => false,
1788
+ };
1789
+ - footer_from_props_lines(
1790
+ - props,
1791
+ - /*collaboration_mode_indicator*/ None,
1792
+ - /*show_cycle_hint*/ false,
1793
+ - show_shortcuts_hint,
1794
+ - show_queue_hint,
1795
+ - )
1796
+ - .len() as u16
1797
+ + props.cxstatusline_block.len() as u16
1798
+ + + footer_from_props_lines(
1799
+ + props,
1800
+ + /*collaboration_mode_indicator*/ None,
1801
+ + /*show_cycle_hint*/ false,
1802
+ + show_shortcuts_hint,
1803
+ + show_queue_hint,
1804
+ + )
1805
+ + .len() as u16
1806
+ }
1807
+
1808
+ /// Render a single precomputed footer line.
1809
+ @@ -257,6 +261,16 @@ pub(crate) fn render_footer_line(area: Rect, buf: &mut Buffer, line: Line<'stati
1810
+ .render(area, buf);
1811
+ }
1812
+
1813
+ +/// Render renderer-owned rows above the native footer content.
1814
+ +pub(crate) fn render_cxstatusline_block(area: Rect, buf: &mut Buffer, block: &[Line<'static>]) {
1815
+ + Paragraph::new(prefix_lines(
1816
+ + block.to_vec(),
1817
+ + " ".repeat(FOOTER_INDENT_COLS).into(),
1818
+ + " ".repeat(FOOTER_INDENT_COLS).into(),
1819
+ + ))
1820
+ + .render(area, buf);
1821
+ +}
1822
+ +
1823
+ /// Render footer content directly from `FooterProps`.
1824
+ ///
1825
+ /// This is intentionally not part of the width-based collapse/fallback logic.
1826
+ @@ -1313,12 +1327,50 @@ mod tests {
1827
+ use ratatui::backend::Backend;
1828
+ use ratatui::backend::TestBackend;
1829
+
1830
+ + fn props_with_status_block(rows: usize, mode: FooterMode) -> FooterProps {
1831
+ + FooterProps {
1832
+ + mode,
1833
+ + esc_backtrack_hint: false,
1834
+ + use_shift_enter_hint: false,
1835
+ + is_task_running: false,
1836
+ + queue_submissions: false,
1837
+ + collaboration_modes_enabled: false,
1838
+ + is_wsl: false,
1839
+ + quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
1840
+ + status_line_value: None,
1841
+ + status_line_enabled: false,
1842
+ + cxstatusline_block: (0..rows)
1843
+ + .map(|row| Line::from(format!("status-{row}")))
1844
+ + .collect(),
1845
+ + key_hints: FooterKeyHints::default_bindings(),
1846
+ + active_agent_label: None,
1847
+ + }
1848
+ + }
1849
+ +
1850
+ fn snapshot_footer(name: &str, props: FooterProps) {
1851
+ snapshot_footer_with_mode_indicator(
1852
+ name, /*width*/ 80, &props, /*collaboration_mode_indicator*/ None,
1853
+ );
1854
+ }
1855
+
1856
+ + #[test]
1857
+ + fn cxstatusline_block_adds_rows_without_hiding_instruction_footer() {
1858
+ + let props = props_with_status_block(3, FooterMode::EscHint);
1859
+ + assert_eq!(footer_height(&props), 4);
1860
+ +
1861
+ + let screen = render_footer_with_mode_indicator_and_context(
1862
+ + /*width*/ 80,
1863
+ + &props,
1864
+ + /*collaboration_mode_indicator*/ None,
1865
+ + context_window_line(/*percent*/ None, /*used_tokens*/ None),
1866
+ + );
1867
+ + let lines = screen.lines().map(str::trim).collect::<Vec<_>>();
1868
+ + assert_eq!(&lines[..3], ["status-0", "status-1", "status-2"]);
1869
+ + assert!(lines[3].contains("esc"));
1870
+ +
1871
+ + snapshot_footer("cxstatusline_block_preserves_instruction_footer", props);
1872
+ + }
1873
+ +
1874
+ fn snapshot_footer_with_context(
1875
+ name: &str,
1876
+ props: FooterProps,
1877
+ @@ -1345,6 +1397,26 @@ mod tests {
1878
+ terminal
1879
+ .draw(|f| {
1880
+ let area = Rect::new(0, 0, f.area().width, height);
1881
+ + let cxstatusline_height = props.cxstatusline_block.len() as u16;
1882
+ + if cxstatusline_height > 0 {
1883
+ + let cxstatusline_area = Rect::new(
1884
+ + area.x,
1885
+ + area.y,
1886
+ + area.width,
1887
+ + cxstatusline_height.min(area.height),
1888
+ + );
1889
+ + render_cxstatusline_block(
1890
+ + cxstatusline_area,
1891
+ + f.buffer_mut(),
1892
+ + &props.cxstatusline_block,
1893
+ + );
1894
+ + }
1895
+ + let area = Rect::new(
1896
+ + area.x,
1897
+ + area.y.saturating_add(cxstatusline_height),
1898
+ + area.width,
1899
+ + area.height.saturating_sub(cxstatusline_height),
1900
+ + );
1901
+ let show_cycle_hint = !props.is_task_running;
1902
+ let show_shortcuts_hint = match props.mode {
1903
+ FooterMode::ComposerEmpty => true,
1904
+ @@ -1594,6 +1666,7 @@ mod tests {
1905
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
1906
+ status_line_value: None,
1907
+ status_line_enabled: false,
1908
+ + cxstatusline_block: Vec::new(),
1909
+ key_hints: FooterKeyHints::default_bindings(),
1910
+ active_agent_label: None,
1911
+ },
1912
+ @@ -1612,6 +1685,7 @@ mod tests {
1913
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
1914
+ status_line_value: None,
1915
+ status_line_enabled: false,
1916
+ + cxstatusline_block: Vec::new(),
1917
+ key_hints: FooterKeyHints {
1918
+ insert_newline: Some(key_hint::shift(KeyCode::Enter).into()),
1919
+ ..FooterKeyHints::default_bindings()
1920
+ @@ -1633,6 +1707,7 @@ mod tests {
1921
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
1922
+ status_line_value: None,
1923
+ status_line_enabled: false,
1924
+ + cxstatusline_block: Vec::new(),
1925
+ key_hints: FooterKeyHints::default_bindings(),
1926
+ active_agent_label: None,
1927
+ },
1928
+ @@ -1651,6 +1726,7 @@ mod tests {
1929
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
1930
+ status_line_value: None,
1931
+ status_line_enabled: false,
1932
+ + cxstatusline_block: Vec::new(),
1933
+ key_hints: FooterKeyHints::default_bindings(),
1934
+ active_agent_label: None,
1935
+ },
1936
+ @@ -1669,6 +1745,7 @@ mod tests {
1937
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
1938
+ status_line_value: None,
1939
+ status_line_enabled: false,
1940
+ + cxstatusline_block: Vec::new(),
1941
+ key_hints: FooterKeyHints::default_bindings(),
1942
+ active_agent_label: None,
1943
+ },
1944
+ @@ -1687,6 +1764,7 @@ mod tests {
1945
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
1946
+ status_line_value: None,
1947
+ status_line_enabled: false,
1948
+ + cxstatusline_block: Vec::new(),
1949
+ key_hints: FooterKeyHints::default_bindings(),
1950
+ active_agent_label: None,
1951
+ },
1952
+ @@ -1705,6 +1783,7 @@ mod tests {
1953
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
1954
+ status_line_value: None,
1955
+ status_line_enabled: false,
1956
+ + cxstatusline_block: Vec::new(),
1957
+ key_hints: FooterKeyHints::default_bindings(),
1958
+ active_agent_label: None,
1959
+ },
1960
+ @@ -1723,6 +1802,7 @@ mod tests {
1961
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
1962
+ status_line_value: None,
1963
+ status_line_enabled: false,
1964
+ + cxstatusline_block: Vec::new(),
1965
+ key_hints: FooterKeyHints::default_bindings(),
1966
+ active_agent_label: None,
1967
+ },
1968
+ @@ -1741,6 +1821,7 @@ mod tests {
1969
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
1970
+ status_line_value: None,
1971
+ status_line_enabled: false,
1972
+ + cxstatusline_block: Vec::new(),
1973
+ key_hints: FooterKeyHints::default_bindings(),
1974
+ active_agent_label: None,
1975
+ },
1976
+ @@ -1761,6 +1842,7 @@ mod tests {
1977
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
1978
+ status_line_value: None,
1979
+ status_line_enabled: false,
1980
+ + cxstatusline_block: Vec::new(),
1981
+ key_hints: FooterKeyHints::default_bindings(),
1982
+ active_agent_label: None,
1983
+ },
1984
+ @@ -1781,6 +1863,7 @@ mod tests {
1985
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
1986
+ status_line_value: None,
1987
+ status_line_enabled: false,
1988
+ + cxstatusline_block: Vec::new(),
1989
+ key_hints: FooterKeyHints::default_bindings(),
1990
+ active_agent_label: None,
1991
+ },
1992
+ @@ -1797,6 +1880,7 @@ mod tests {
1993
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
1994
+ status_line_value: None,
1995
+ status_line_enabled: false,
1996
+ + cxstatusline_block: Vec::new(),
1997
+ key_hints: FooterKeyHints::default_bindings(),
1998
+ active_agent_label: None,
1999
+ };
2000
+ @@ -1826,6 +1910,7 @@ mod tests {
2001
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
2002
+ status_line_value: None,
2003
+ status_line_enabled: false,
2004
+ + cxstatusline_block: Vec::new(),
2005
+ key_hints: FooterKeyHints::default_bindings(),
2006
+ active_agent_label: None,
2007
+ };
2008
+ @@ -1848,6 +1933,7 @@ mod tests {
2009
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
2010
+ status_line_value: Some(Line::from("Status line content".to_string())),
2011
+ status_line_enabled: true,
2012
+ + cxstatusline_block: Vec::new(),
2013
+ key_hints: FooterKeyHints::default_bindings(),
2014
+ active_agent_label: None,
2015
+ };
2016
+ @@ -1865,6 +1951,7 @@ mod tests {
2017
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
2018
+ status_line_value: Some(Line::from("Status line content".to_string())),
2019
+ status_line_enabled: true,
2020
+ + cxstatusline_block: Vec::new(),
2021
+ key_hints: FooterKeyHints::default_bindings(),
2022
+ active_agent_label: None,
2023
+ };
2024
+ @@ -1882,6 +1969,7 @@ mod tests {
2025
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
2026
+ status_line_value: Some(Line::from("Status line content".to_string())),
2027
+ status_line_enabled: true,
2028
+ + cxstatusline_block: Vec::new(),
2029
+ key_hints: FooterKeyHints::default_bindings(),
2030
+ active_agent_label: None,
2031
+ };
2032
+ @@ -1899,6 +1987,7 @@ mod tests {
2033
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
2034
+ status_line_value: None, // command timed out / empty
2035
+ status_line_enabled: true,
2036
+ + cxstatusline_block: Vec::new(),
2037
+ key_hints: FooterKeyHints::default_bindings(),
2038
+ active_agent_label: None,
2039
+ };
2040
+ @@ -1930,6 +2019,7 @@ mod tests {
2041
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
2042
+ status_line_value: None,
2043
+ status_line_enabled: false,
2044
+ + cxstatusline_block: Vec::new(),
2045
+ key_hints: FooterKeyHints::default_bindings(),
2046
+ active_agent_label: None,
2047
+ };
2048
+ @@ -1953,6 +2043,7 @@ mod tests {
2049
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
2050
+ status_line_value: None,
2051
+ status_line_enabled: true,
2052
+ + cxstatusline_block: Vec::new(),
2053
+ key_hints: FooterKeyHints::default_bindings(),
2054
+ active_agent_label: None,
2055
+ };
2056
+ @@ -1979,6 +2070,7 @@ mod tests {
2057
+ "Status line content that should truncate before the mode indicator".to_string(),
2058
+ )),
2059
+ status_line_enabled: true,
2060
+ + cxstatusline_block: Vec::new(),
2061
+ key_hints: FooterKeyHints::default_bindings(),
2062
+ active_agent_label: None,
2063
+ };
2064
+ @@ -2002,6 +2094,7 @@ mod tests {
2065
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
2066
+ status_line_value: None,
2067
+ status_line_enabled: false,
2068
+ + cxstatusline_block: Vec::new(),
2069
+ key_hints: FooterKeyHints::default_bindings(),
2070
+ active_agent_label: Some("Robie [explorer]".to_string()),
2071
+ };
2072
+ @@ -2019,6 +2112,7 @@ mod tests {
2073
+ quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
2074
+ status_line_value: Some(Line::from("Status line content".to_string())),
2075
+ status_line_enabled: true,
2076
+ + cxstatusline_block: Vec::new(),
2077
+ key_hints: FooterKeyHints::default_bindings(),
2078
+ active_agent_label: Some("Robie [explorer]".to_string()),
2079
+ };
2080
+ @@ -2042,6 +2136,7 @@ mod tests {
2081
+ .to_string(),
2082
+ )),
2083
+ status_line_enabled: true,
2084
+ + cxstatusline_block: Vec::new(),
2085
+ key_hints: FooterKeyHints::default_bindings(),
2086
+ active_agent_label: None,
2087
+ };
2088
+ diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs
2089
+ index f249fad8b2..f7ac935414 100644
2090
+ --- a/codex-rs/tui/src/bottom_pane/mod.rs
2091
+ +++ b/codex-rs/tui/src/bottom_pane/mod.rs
2092
+ @@ -2095,6 +2095,12 @@ impl BottomPane {
2093
+ }
2094
+ }
2095
+
2096
+ + pub(crate) fn set_cxstatusline_block(&mut self, block: Vec<Line<'static>>) {
2097
+ + if self.composer.set_cxstatusline_block(block) {
2098
+ + self.request_redraw();
2099
+ + }
2100
+ + }
2101
+ +
2102
+ /// Updates the contextual footer label and requests a redraw only when it changed.
2103
+ ///
2104
+ /// This keeps the footer plumbing cheap during thread transitions where `App` may recompute
2105
+ diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__cxstatusline_block_preserves_instruction_footer.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__cxstatusline_block_preserves_instruction_footer.snap
2106
+ new file mode 100644
2107
+ index 0000000000..abf66c5ec2
2108
+ --- /dev/null
2109
+ +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__cxstatusline_block_preserves_instruction_footer.snap
2110
+ @@ -0,0 +1,8 @@
2111
+ +---
2112
+ +source: tui/src/bottom_pane/footer.rs
2113
+ +expression: terminal.backend()
2114
+ +---
2115
+ +" status-0 "
2116
+ +" status-1 "
2117
+ +" status-2 "
2118
+ +" esc esc to edit previous message "
2119
+ diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs
2120
+ index 16d4a6e593..4b358b6144 100644
2121
+ --- a/codex-rs/tui/src/chatwidget.rs
2122
+ +++ b/codex-rs/tui/src/chatwidget.rs
2123
+ @@ -789,6 +789,24 @@ pub(crate) struct ChatWidget {
2124
+ status_line_workspace_headline_last_requested_at: Option<Instant>,
2125
+ // Set after the backend reports the workspace-message feature gate is disabled.
2126
+ status_line_workspace_messages_disabled: bool,
2127
+ + // cxstatusline: argv of the external renderer from `CXSTATUSLINE_COMMAND`, if set.
2128
+ + cxstatusline_command: Option<Vec<String>>,
2129
+ + // cxstatusline: last successfully rendered complete block, shared with the render task.
2130
+ + cxstatusline_output: Arc<std::sync::Mutex<Option<Vec<Line<'static>>>>>,
2131
+ + // cxstatusline: true while a render task is in flight.
2132
+ + cxstatusline_pending: Arc<AtomicBool>,
2133
+ + // cxstatusline: set by the render task when new output landed - draw it, do not re-spawn.
2134
+ + cxstatusline_fresh: Arc<AtomicBool>,
2135
+ + // cxstatusline: a refresh arrived while throttled or pending - re-run once possible.
2136
+ + cxstatusline_dirty: bool,
2137
+ + // cxstatusline: last time a render task was spawned (throttle).
2138
+ + cxstatusline_last_spawn_at: Option<Instant>,
2139
+ + // cxstatusline: true while the periodic account/rateLimits/read is in flight.
2140
+ + cxstatusline_rate_limit_pending: bool,
2141
+ + // cxstatusline: last periodic account/rateLimits/read request time (throttle).
2142
+ + cxstatusline_rate_limit_last_requested_at: Option<Instant>,
2143
+ + // cxstatusline: captured once so renderer speed calculations cover this TUI session.
2144
+ + cxstatusline_started_at: chrono::DateTime<chrono::Utc>,
2145
+ // Cached backend-estimated cost and bounded refresh state for the current thread.
2146
+ thread_usage: thread_usage::ThreadUsageState,
2147
+ // Current thread-goal status shown in the status line when plan mode is inactive.
2148
+ @@ -1191,6 +1209,7 @@ impl ChatWidget {
2149
+ self.refresh_terminal_title();
2150
+ }
2151
+ self.refresh_status_line_if_workspace_headline_due();
2152
+ + self.cxstatusline_pre_draw_tick();
2153
+ self.refresh_thread_usage_if_settlement_due();
2154
+ }
2155
+
2156
+ diff --git a/codex-rs/tui/src/chatwidget/constructor.rs b/codex-rs/tui/src/chatwidget/constructor.rs
2157
+ index f45ae82fef..49c0c96e27 100644
2158
+ --- a/codex-rs/tui/src/chatwidget/constructor.rs
2159
+ +++ b/codex-rs/tui/src/chatwidget/constructor.rs
2160
+ @@ -263,6 +263,19 @@ impl ChatWidget {
2161
+ next_status_line_workspace_headline_request_id: 0,
2162
+ status_line_workspace_headline_last_requested_at: None,
2163
+ status_line_workspace_messages_disabled: false,
2164
+ + cxstatusline_command: std::env::var("CXSTATUSLINE_COMMAND")
2165
+ + .ok()
2166
+ + .filter(|raw| !raw.trim().is_empty())
2167
+ + .and_then(|raw| shlex::split(&raw))
2168
+ + .filter(|argv| !argv.is_empty()),
2169
+ + cxstatusline_output: Arc::new(std::sync::Mutex::new(None)),
2170
+ + cxstatusline_pending: Arc::new(AtomicBool::new(false)),
2171
+ + cxstatusline_fresh: Arc::new(AtomicBool::new(false)),
2172
+ + cxstatusline_dirty: false,
2173
+ + cxstatusline_last_spawn_at: None,
2174
+ + cxstatusline_rate_limit_pending: false,
2175
+ + cxstatusline_rate_limit_last_requested_at: None,
2176
+ + cxstatusline_started_at: chrono::Utc::now(),
2177
+ thread_usage: thread_usage::ThreadUsageState::default(),
2178
+ current_goal_status_indicator: None,
2179
+ current_goal_status: None,
2180
+ diff --git a/codex-rs/tui/src/chatwidget/status_surfaces.rs b/codex-rs/tui/src/chatwidget/status_surfaces.rs
2181
+ index 17f437c488..cc6451b696 100644
2182
+ --- a/codex-rs/tui/src/chatwidget/status_surfaces.rs
2183
+ +++ b/codex-rs/tui/src/chatwidget/status_surfaces.rs
2184
+ @@ -42,6 +42,16 @@ const TERMINAL_TITLE_ACTION_REQUIRED_INTERVAL: Duration = Duration::from_secs(1)
2185
+ const TERMINAL_TITLE_ACTION_REQUIRED_PREFIX: &str = "[ ! ] Action Required";
2186
+ const TERMINAL_TITLE_ACTION_REQUIRED_PREFIX_HIDDEN: &str = "[ . ] Action Required";
2187
+
2188
+ +/// cxstatusline: minimum spacing between renderer spawns. 18 refresh call sites; one process per
2189
+ +/// call would be waste. Refreshes inside the window coalesce and run once it closes.
2190
+ +const CXSTATUSLINE_MIN_INTERVAL: Duration = Duration::from_millis(200);
2191
+ +
2192
+ +/// cxstatusline: renderer wall-clock budget. Over budget -> killed, last good line kept.
2193
+ +const CXSTATUSLINE_TIMEOUT: Duration = Duration::from_secs(1);
2194
+ +
2195
+ +/// cxstatusline: bounded account-rate-limit freshness for renderer payloads.
2196
+ +const CXSTATUSLINE_RATE_LIMIT_REFRESH_INTERVAL: Duration = Duration::from_secs(/*secs*/ 60);
2197
+ +
2198
+ #[derive(Debug)]
2199
+ /// Parsed status-surface configuration for one refresh pass.
2200
+ ///
2201
+ @@ -159,7 +169,7 @@ impl ChatWidget {
2202
+ }
2203
+
2204
+ fn sync_status_surface_shared_state(&mut self, selections: &StatusSurfaceSelections) {
2205
+ - if !selections.uses_git_branch() {
2206
+ + if !selections.uses_git_branch() && self.cxstatusline_command.is_none() {
2207
+ self.status_line_branch = None;
2208
+ self.status_line_branch_pending = false;
2209
+ self.status_line_branch_lookup_complete = false;
2210
+ @@ -171,7 +181,7 @@ impl ChatWidget {
2211
+ }
2212
+ }
2213
+
2214
+ - if !selections.uses_git_summary() {
2215
+ + if !selections.uses_git_summary() && self.cxstatusline_command.is_none() {
2216
+ self.status_line_git_summary = None;
2217
+ self.status_line_git_summary_pending = false;
2218
+ self.status_line_git_summary_lookup_complete = false;
2219
+ @@ -200,6 +210,12 @@ impl ChatWidget {
2220
+ }
2221
+
2222
+ fn refresh_status_line_from_selections(&mut self, selections: &StatusSurfaceSelections) {
2223
+ + if self.cxstatusline_command.is_some() {
2224
+ + self.cxstatusline_request_render();
2225
+ + if self.cxstatusline_draw_shared_cell() {
2226
+ + return;
2227
+ + }
2228
+ + }
2229
+ let enabled = !selections.status_line_items.is_empty();
2230
+ self.bottom_pane.set_status_line_enabled(enabled);
2231
+ if !enabled {
2232
+ @@ -421,7 +437,7 @@ impl ChatWidget {
2233
+
2234
+ pub(super) fn request_status_line_branch_refresh(&mut self) {
2235
+ let selections = self.status_surface_selections();
2236
+ - if !selections.uses_git_branch() {
2237
+ + if !selections.uses_git_branch() && self.cxstatusline_command.is_none() {
2238
+ return;
2239
+ }
2240
+ let cwd = self.status_line_cwd().to_path_buf();
2241
+ @@ -431,7 +447,7 @@ impl ChatWidget {
2242
+
2243
+ pub(super) fn request_status_line_git_summary_refresh(&mut self) {
2244
+ let selections = self.status_surface_selections();
2245
+ - if !selections.uses_git_summary() {
2246
+ + if !selections.uses_git_summary() && self.cxstatusline_command.is_none() {
2247
+ return;
2248
+ }
2249
+ let cwd = self.status_line_cwd().to_path_buf();
2250
+ @@ -582,6 +598,189 @@ impl ChatWidget {
2251
+ ///
2252
+ /// The resulting `StatusLineBranchUpdated` event carries the lookup cwd so callers can reject
2253
+ /// stale completions after directory changes.
2254
+ + /// cxstatusline: called from `pre_draw_tick`. Draws fresh renderer output without spawning,
2255
+ + /// and replays one coalesced refresh once the throttle window has closed.
2256
+ + pub(super) fn cxstatusline_pre_draw_tick(&mut self) {
2257
+ + if self.cxstatusline_command.is_none() {
2258
+ + return;
2259
+ + }
2260
+ + self.cxstatusline_rate_limit_tick(Instant::now());
2261
+ + if self.cxstatusline_fresh.swap(false, Ordering::AcqRel) {
2262
+ + self.cxstatusline_draw_shared_cell();
2263
+ + }
2264
+ + if !self.cxstatusline_dirty || self.cxstatusline_pending.load(Ordering::Acquire) {
2265
+ + return;
2266
+ + }
2267
+ + if let Some(remaining) = self.cxstatusline_throttle_remaining(Instant::now()) {
2268
+ + self.frame_requester.schedule_frame_in(remaining);
2269
+ + return;
2270
+ + }
2271
+ + self.cxstatusline_dirty = false;
2272
+ + self.refresh_status_line();
2273
+ + }
2274
+ +
2275
+ + /// Requests fresh account limits for renderer payloads at most once per minute.
2276
+ + pub(super) fn cxstatusline_rate_limit_tick(&mut self, now: Instant) {
2277
+ + if self.cxstatusline_command.is_none()
2278
+ + || !self.should_prefetch_rate_limits()
2279
+ + || self.cxstatusline_rate_limit_pending
2280
+ + {
2281
+ + return;
2282
+ + }
2283
+ + if let Some(last_requested_at) = self.cxstatusline_rate_limit_last_requested_at {
2284
+ + let elapsed = now.saturating_duration_since(last_requested_at);
2285
+ + if elapsed < CXSTATUSLINE_RATE_LIMIT_REFRESH_INTERVAL {
2286
+ + self.frame_requester
2287
+ + .schedule_frame_in(CXSTATUSLINE_RATE_LIMIT_REFRESH_INTERVAL - elapsed);
2288
+ + return;
2289
+ + }
2290
+ + }
2291
+ + self.cxstatusline_rate_limit_pending = true;
2292
+ + self.cxstatusline_rate_limit_last_requested_at = Some(now);
2293
+ + self.app_event_tx.send(AppEvent::RefreshRateLimits {
2294
+ + origin: crate::app_event::RateLimitRefreshOrigin::CxStatusLine,
2295
+ + });
2296
+ + self.frame_requester
2297
+ + .schedule_frame_in(CXSTATUSLINE_RATE_LIMIT_REFRESH_INTERVAL);
2298
+ + }
2299
+ +
2300
+ + pub(crate) fn finish_cxstatusline_rate_limit_refresh(&mut self) {
2301
+ + self.cxstatusline_rate_limit_pending = false;
2302
+ + // The completed read changed the payload source; schedule one renderer run.
2303
+ + self.cxstatusline_dirty = true;
2304
+ + }
2305
+ +
2306
+ + /// cxstatusline: draw the last complete shared block, if any. A poisoned lock leaves the
2307
+ + /// already-rendered block untouched, so callers do not replace it with partial/native output.
2308
+ + fn cxstatusline_draw_shared_cell(&mut self) -> bool {
2309
+ + let rendered = match self.cxstatusline_output.lock() {
2310
+ + Ok(guard) => guard.clone(),
2311
+ + Err(_) => return true,
2312
+ + };
2313
+ + let Some(rendered) = rendered else {
2314
+ + return false;
2315
+ + };
2316
+ + self.bottom_pane.set_cxstatusline_block(rendered);
2317
+ + self.bottom_pane.set_status_line_enabled(/*enabled*/ false);
2318
+ + self.set_status_line(/*status_line*/ None);
2319
+ + self.set_status_line_hyperlink(/*url*/ None);
2320
+ + true
2321
+ + }
2322
+ +
2323
+ + fn cxstatusline_throttle_remaining(&self, now: Instant) -> Option<Duration> {
2324
+ + let last = self.cxstatusline_last_spawn_at?;
2325
+ + let elapsed = now.saturating_duration_since(last);
2326
+ + (elapsed < CXSTATUSLINE_MIN_INTERVAL).then(|| CXSTATUSLINE_MIN_INTERVAL - elapsed)
2327
+ + }
2328
+ +
2329
+ + /// cxstatusline: spawn the renderer with a payload v1 snapshot, unless one is in flight or the
2330
+ + /// throttle window is open - then remember to run again. Mirrors `request_status_line_branch`.
2331
+ + fn cxstatusline_request_render(&mut self) {
2332
+ + let Some(argv) = self.cxstatusline_command.clone() else {
2333
+ + return;
2334
+ + };
2335
+ + let now = Instant::now();
2336
+ + if self.cxstatusline_pending.load(Ordering::Acquire)
2337
+ + || self.cxstatusline_throttle_remaining(now).is_some()
2338
+ + {
2339
+ + self.cxstatusline_dirty = true;
2340
+ + self.frame_requester
2341
+ + .schedule_frame_in(CXSTATUSLINE_MIN_INTERVAL);
2342
+ + return;
2343
+ + }
2344
+ + let payload = self.cxstatusline_payload_v1().to_string();
2345
+ + self.cxstatusline_pending.store(true, Ordering::Release);
2346
+ + self.cxstatusline_last_spawn_at = Some(now);
2347
+ + let output = self.cxstatusline_output.clone();
2348
+ + let pending = self.cxstatusline_pending.clone();
2349
+ + let fresh = self.cxstatusline_fresh.clone();
2350
+ + let frame_requester = self.frame_requester.clone();
2351
+ + tokio::spawn(async move {
2352
+ + if let Some(block) = cxstatusline_run(&argv, payload).await
2353
+ + && let Ok(mut guard) = output.lock()
2354
+ + {
2355
+ + *guard = Some(block);
2356
+ + fresh.store(true, Ordering::Release);
2357
+ + }
2358
+ + pending.store(false, Ordering::Release);
2359
+ + frame_requester.schedule_frame();
2360
+ + });
2361
+ + }
2362
+ +
2363
+ + /// cxstatusline: typed snapshot of the git state behind the native git status-line items.
2364
+ + fn cxstatusline_git_v1(&self) -> serde_json::Value {
2365
+ + use serde_json::json;
2366
+ + let summary = self.status_line_git_summary.as_ref();
2367
+ + json!({
2368
+ + "branch": self.status_line_branch.as_deref(),
2369
+ + "changes": summary
2370
+ + .and_then(|summary| summary.branch_change_stats.as_ref())
2371
+ + .map(|stats| json!({ "additions": stats.additions, "deletions": stats.deletions })),
2372
+ + "pr": summary
2373
+ + .and_then(|summary| summary.pull_request.as_ref())
2374
+ + .map(|pull_request| pull_request.number),
2375
+ + })
2376
+ + }
2377
+ +
2378
+ + /// cxstatusline: typed snapshot of the state behind every native status-line item.
2379
+ + /// `null` means "unavailable right now", same as an absent key. Enum-valued fields carry
2380
+ + /// Codex's own serde/strum identifiers (`on-request`, `workspace-write`), never display text.
2381
+ + fn cxstatusline_payload_v1(&self) -> serde_json::Value {
2382
+ + use serde_json::json;
2383
+ + let cwd = self.status_line_cwd().to_path_buf();
2384
+ + let project_root = self.status_line_project_root_for_cwd(&cwd);
2385
+ + let codex_limits = self.rate_limit_snapshots_by_limit_id.get("codex");
2386
+ + let window = |w: Option<(&RateLimitWindowDisplay, bool)>| {
2387
+ + w.map(|(window, _is_secondary)| {
2388
+ + json!({
2389
+ + "used": window.used_percent / 100.0,
2390
+ + "resets_at": window.resets_at_unix.and_then(cxstatusline_iso8601),
2391
+ + })
2392
+ + })
2393
+ + };
2394
+ + let used_tokens = {
2395
+ + let total = self.status_line_total_usage().blended_total();
2396
+ + (total > 0).then_some(total)
2397
+ + };
2398
+ + let usage = self.token_info.as_ref().map(|info| &info.total_token_usage);
2399
+ + let context_tokens = self
2400
+ + .token_info
2401
+ + .as_ref()
2402
+ + .map(|info| info.last_token_usage.tokens_in_context_window());
2403
+ + json!({
2404
+ + "payload_version": 1,
2405
+ + "model": {
2406
+ + "name": self.model_display_name(),
2407
+ + "reasoning": self.reasoning_display_name(),
2408
+ + },
2409
+ + "git": self.cxstatusline_git_v1(),
2410
+ + "usage": {
2411
+ + "context_used": self.status_line_context_used_percent().map(|p| p as f64 / 100.0),
2412
+ + "context_remaining": self.status_line_context_remaining_percent().map(|p| p as f64 / 100.0),
2413
+ + "context_tokens": context_tokens,
2414
+ + "context_window": self.status_line_context_window_size(),
2415
+ + "used_tokens": used_tokens,
2416
+ + "input_tokens": usage.map(|usage| usage.input_tokens),
2417
+ + "output_tokens": usage.map(|usage| usage.output_tokens),
2418
+ + "cached_input_tokens": usage.map(|usage| usage.cached_input_tokens),
2419
+ + "five_hour": window(codex_limits.and_then(five_hour_status_window)),
2420
+ + "weekly": window(codex_limits.and_then(weekly_status_window)),
2421
+ + },
2422
+ + "session": {
2423
+ + "id": self.thread_id.map(|id| id.to_string()),
2424
+ + "cwd": cwd.to_string_lossy(),
2425
+ + "project_root": project_root.map(|p| p.to_string_lossy().into_owned()),
2426
+ + "hostname": os_host_name(),
2427
+ + "approval_mode": self.config.permissions.approval_policy.value().to_string(),
2428
+ + "permissions": self.config.legacy_sandbox_policy().to_string(),
2429
+ + "run_state": self.run_state_status_text().to_lowercase(),
2430
+ + "codex_version": crate::version::CODEX_CLI_VERSION,
2431
+ + "started_at": self.cxstatusline_started_at.to_rfc3339_opts(chrono::SecondsFormat::Secs, /*use_z*/ true),
2432
+ + "thread_title": self.thread_name.clone(),
2433
+ + },
2434
+ + })
2435
+ + }
2436
+ +
2437
+ fn request_status_line_branch(&mut self, cwd: PathBuf) {
2438
+ if self.status_line_branch_pending {
2439
+ return;
2440
+ @@ -1243,3 +1442,48 @@ where
2441
+ }
2442
+ (items, invalid)
2443
+ }
2444
+ +
2445
+ +/// cxstatusline: seconds since the Unix epoch -> RFC 3339 UTC.
2446
+ +fn cxstatusline_iso8601(seconds: i64) -> Option<String> {
2447
+ + chrono::DateTime::<chrono::Utc>::from_timestamp(seconds, 0)
2448
+ + .map(|dt| dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, /*use_z*/ true))
2449
+ +}
2450
+ +
2451
+ +/// cxstatusline: run the external renderer once. `None` on spawn failure, non-zero exit, timeout,
2452
+ +/// malformed, blank, or oversized stdout - the caller keeps the previous complete block.
2453
+ +async fn cxstatusline_run(argv: &[String], payload: String) -> Option<Vec<Line<'static>>> {
2454
+ + use tokio::io::AsyncWriteExt;
2455
+ + let (program, args) = argv.split_first()?;
2456
+ + let mut child = tokio::process::Command::new(program)
2457
+ + .args(args)
2458
+ + .stdin(std::process::Stdio::piped())
2459
+ + .stdout(std::process::Stdio::piped())
2460
+ + .stderr(std::process::Stdio::null())
2461
+ + .kill_on_drop(true)
2462
+ + .spawn()
2463
+ + .ok()?;
2464
+ + let mut stdin = child.stdin.take()?;
2465
+ + stdin.write_all(payload.as_bytes()).await.ok()?;
2466
+ + // Drop closes the pipe so the renderer sees EOF.
2467
+ + drop(stdin);
2468
+ + let output = tokio::time::timeout(CXSTATUSLINE_TIMEOUT, child.wait_with_output())
2469
+ + .await
2470
+ + .ok()?
2471
+ + .ok()?;
2472
+ + if !output.status.success() {
2473
+ + return None;
2474
+ + }
2475
+ + cxstatusline_parse_output(&String::from_utf8_lossy(&output.stdout))
2476
+ +}
2477
+ +
2478
+ +/// Parse all renderer rows before exposing them to the shared block; a malformed block never
2479
+ +/// replaces the last complete render.
2480
+ +pub(super) fn cxstatusline_parse_output(text: &str) -> Option<Vec<Line<'static>>> {
2481
+ + let text = text.strip_suffix('\n').unwrap_or(text);
2482
+ + let rows: Vec<_> = text.split('\n').collect();
2483
+ + ((1..=3).contains(&rows.len()) && rows.iter().all(|line| !line.trim().is_empty())).then(|| {
2484
+ + rows.into_iter()
2485
+ + .map(codex_ansi_escape::ansi_escape_line)
2486
+ + .collect()
2487
+ + })
2488
+ +}
2489
+ diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs
2490
+ index 6e68f7795e..02a10e94ea 100644
2491
+ --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs
2492
+ +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs
2493
+ @@ -1,4 +1,8 @@
2494
+ +use std::time::Duration;
2495
+ +use std::time::Instant;
2496
+ +
2497
+ use super::*;
2498
+ +use crate::app_event::RateLimitRefreshOrigin;
2499
+ use crate::bottom_pane::goal_status_indicator_line;
2500
+ use crate::chatwidget::ThreadUsageOutcome;
2501
+ use crate::chatwidget::rate_limits::NUDGE_MODEL_SLUG;
2502
+ @@ -9,6 +13,245 @@ use pretty_assertions::assert_eq;
2503
+ use ratatui::backend::TestBackend;
2504
+ use serial_test::serial;
2505
+
2506
+ +struct CxLocalGitRunner;
2507
+ +
2508
+ +impl crate::workspace_command::WorkspaceCommandExecutor for CxLocalGitRunner {
2509
+ + fn run(
2510
+ + &self,
2511
+ + command: crate::workspace_command::WorkspaceCommand,
2512
+ + ) -> std::pin::Pin<
2513
+ + Box<
2514
+ + dyn std::future::Future<
2515
+ + Output = Result<
2516
+ + crate::workspace_command::WorkspaceCommandOutput,
2517
+ + crate::workspace_command::WorkspaceCommandError,
2518
+ + >,
2519
+ + > + Send
2520
+ + + '_,
2521
+ + >,
2522
+ + > {
2523
+ + Box::pin(async move {
2524
+ + // Only local Git runs: PR probes must never contact the owner's GitHub account.
2525
+ + if command.argv.first().map(String::as_str) != Some("git") {
2526
+ + return Ok(crate::workspace_command::WorkspaceCommandOutput {
2527
+ + exit_code: 1,
2528
+ + stdout: String::new(),
2529
+ + stderr: String::new(),
2530
+ + });
2531
+ + }
2532
+ + let mut process = tokio::process::Command::new("git");
2533
+ + process.args(&command.argv[1..]).kill_on_drop(true);
2534
+ + if let Some(cwd) = command.cwd {
2535
+ + process.current_dir(cwd);
2536
+ + }
2537
+ + for (key, value) in command.env {
2538
+ + if let Some(value) = value {
2539
+ + process.env(key, value);
2540
+ + } else {
2541
+ + process.env_remove(key);
2542
+ + }
2543
+ + }
2544
+ + let output = tokio::time::timeout(command.timeout, process.output())
2545
+ + .await
2546
+ + .expect("git timeout")
2547
+ + .expect("git runs");
2548
+ + Ok(crate::workspace_command::WorkspaceCommandOutput {
2549
+ + exit_code: output.status.code().unwrap_or(1),
2550
+ + stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
2551
+ + stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
2552
+ + })
2553
+ + })
2554
+ + }
2555
+ +}
2556
+ +
2557
+ +#[tokio::test]
2558
+ +async fn cxstatusline_git_values_change_after_a_turn() {
2559
+ + let repo = tempfile::tempdir().expect("temp repo");
2560
+ + let git = |args: &[&str]| {
2561
+ + let output = std::process::Command::new("git")
2562
+ + .args(args)
2563
+ + .current_dir(repo.path())
2564
+ + .output()
2565
+ + .expect("git runs");
2566
+ + assert!(
2567
+ + output.status.success(),
2568
+ + "{}",
2569
+ + String::from_utf8_lossy(&output.stderr)
2570
+ + );
2571
+ + };
2572
+ + git(&["init", "-b", "main"]);
2573
+ + git(&[
2574
+ + "-c",
2575
+ + "user.name=Test",
2576
+ + "-c",
2577
+ + "user.email=test@example.com",
2578
+ + "commit",
2579
+ + "--allow-empty",
2580
+ + "-m",
2581
+ + "base",
2582
+ + ]);
2583
+ + let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
2584
+ + chat.config.cwd = AbsolutePathBuf::try_from(repo.path().to_path_buf()).expect("absolute cwd");
2585
+ + chat.current_cwd = Some(repo.path().to_path_buf());
2586
+ + chat.local_settings.tui.status_line = Some(vec![]);
2587
+ + chat.local_settings.tui.terminal_title = Some(vec![]);
2588
+ + chat.cxstatusline_command = Some(vec!["unused-renderer".into()]);
2589
+ + // Exercise collection without launching a renderer during completion callbacks.
2590
+ + chat.cxstatusline_pending.store(true, Ordering::Release);
2591
+ + chat.workspace_command_runner = Some(std::sync::Arc::new(CxLocalGitRunner));
2592
+ + for (branch, changes) in [("main", "No changes"), ("feature", "+1 -0")] {
2593
+ + if branch == "feature" {
2594
+ + git(&["checkout", "-b", "feature"]);
2595
+ + std::fs::write(repo.path().join("file"), "one line\n").expect("write change");
2596
+ + git(&["add", "file"]);
2597
+ + git(&[
2598
+ + "-c",
2599
+ + "user.name=Test",
2600
+ + "-c",
2601
+ + "user.email=test@example.com",
2602
+ + "commit",
2603
+ + "-m",
2604
+ + "change",
2605
+ + ]);
2606
+ + }
2607
+ + handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None);
2608
+ + assert!(chat.status_line_branch_pending && chat.status_line_git_summary_pending);
2609
+ + tokio::time::timeout(Duration::from_secs(/*secs*/ 10), async {
2610
+ + while chat.status_line_branch_pending || chat.status_line_git_summary_pending {
2611
+ + match rx.recv().await.expect("completion event") {
2612
+ + AppEvent::StatusLineBranchUpdated { cwd, branch } => {
2613
+ + chat.set_status_line_branch(cwd, branch)
2614
+ + }
2615
+ + AppEvent::StatusLineGitSummaryUpdated { cwd, summary } => {
2616
+ + chat.set_status_line_git_summary(cwd, summary)
2617
+ + }
2618
+ + _ => {}
2619
+ + }
2620
+ + }
2621
+ + })
2622
+ + .await
2623
+ + .expect("Git refresh completes");
2624
+ + assert_eq!(chat.status_line_branch.as_deref(), Some(branch));
2625
+ + assert_eq!(
2626
+ + chat.status_line_value_for_item(crate::bottom_pane::StatusLineItem::BranchChanges),
2627
+ + Some(changes.into())
2628
+ + );
2629
+ + }
2630
+ +}
2631
+ +
2632
+ +#[tokio::test]
2633
+ +async fn cxstatusline_git_refresh_does_not_require_native_items() {
2634
+ + for mode in ["disabled", "cx", "native"] {
2635
+ + let (mut chat, _rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
2636
+ + install_noop_workspace_command_runner(&mut chat);
2637
+ + chat.local_settings.tui.status_line = Some(if mode == "native" {
2638
+ + vec!["git-branch".into(), "branch-changes".into()]
2639
+ + } else {
2640
+ + vec![]
2641
+ + });
2642
+ + chat.local_settings.tui.terminal_title = Some(vec![]);
2643
+ + chat.cxstatusline_command = (mode == "cx").then(|| vec!["unused-renderer".into()]);
2644
+ + let cwd = chat.config.cwd.to_path_buf();
2645
+ + chat.status_line_branch_cwd = Some(cwd.clone());
2646
+ + chat.status_line_git_summary_cwd = Some(cwd);
2647
+ + chat.status_line_branch_lookup_complete = true;
2648
+ + chat.status_line_git_summary_lookup_complete = true;
2649
+ + chat.request_status_line_branch_refresh();
2650
+ + chat.request_status_line_git_summary_refresh();
2651
+ + assert_eq!(
2652
+ + (
2653
+ + chat.status_line_branch_pending,
2654
+ + chat.status_line_git_summary_pending
2655
+ + ),
2656
+ + (mode != "disabled", mode != "disabled"),
2657
+ + "refresh mode {mode}"
2658
+ + );
2659
+ + }
2660
+ +}
2661
+ +
2662
+ +#[test]
2663
+ +fn cxstatusline_accepts_complete_rows_and_rejects_invalid_blocks() {
2664
+ + assert_eq!(
2665
+ + super::super::status_surfaces::cxstatusline_parse_output("one\ntwo\nthree\n")
2666
+ + .expect("three non-blank rows")
2667
+ + .len(),
2668
+ + 3
2669
+ + );
2670
+ + assert!(super::super::status_surfaces::cxstatusline_parse_output("one\n\nthree\n").is_none());
2671
+ + assert!(super::super::status_surfaces::cxstatusline_parse_output("one\ntwo\n\n").is_none());
2672
+ + assert!(
2673
+ + super::super::status_surfaces::cxstatusline_parse_output("one\ntwo\nthree\nfour\n")
2674
+ + .is_none()
2675
+ + );
2676
+ +}
2677
+ +
2678
+ +#[tokio::test]
2679
+ +async fn cxstatusline_rate_limit_tick_requires_command_and_chatgpt_auth() {
2680
+ + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
2681
+ + let now = Instant::now();
2682
+ +
2683
+ + chat.cxstatusline_rate_limit_tick(now);
2684
+ + assert!(matches!(
2685
+ + rx.try_recv(),
2686
+ + Err(tokio::sync::mpsc::error::TryRecvError::Empty)
2687
+ + ));
2688
+ +
2689
+ + chat.cxstatusline_command = Some(vec!["renderer".to_string()]);
2690
+ + chat.cxstatusline_rate_limit_tick(now);
2691
+ + assert!(matches!(
2692
+ + rx.try_recv(),
2693
+ + Err(tokio::sync::mpsc::error::TryRecvError::Empty)
2694
+ + ));
2695
+ +
2696
+ + set_chatgpt_auth(&mut chat);
2697
+ + chat.cxstatusline_rate_limit_tick(now);
2698
+ + assert!(matches!(
2699
+ + rx.try_recv(),
2700
+ + Ok(AppEvent::RefreshRateLimits {
2701
+ + origin: RateLimitRefreshOrigin::CxStatusLine,
2702
+ + })
2703
+ + ));
2704
+ +}
2705
+ +
2706
+ +#[tokio::test]
2707
+ +async fn cxstatusline_rate_limit_completion_releases_next_minute_tick() {
2708
+ + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
2709
+ + chat.cxstatusline_command = Some(vec!["renderer".to_string()]);
2710
+ + set_chatgpt_auth(&mut chat);
2711
+ + let now = Instant::now();
2712
+ +
2713
+ + chat.cxstatusline_rate_limit_tick(now);
2714
+ + assert!(matches!(
2715
+ + rx.try_recv(),
2716
+ + Ok(AppEvent::RefreshRateLimits {
2717
+ + origin: RateLimitRefreshOrigin::CxStatusLine,
2718
+ + })
2719
+ + ));
2720
+ + assert!(chat.cxstatusline_rate_limit_pending);
2721
+ +
2722
+ + chat.cxstatusline_rate_limit_tick(now + Duration::from_secs(/*secs*/ 59));
2723
+ + assert!(matches!(
2724
+ + rx.try_recv(),
2725
+ + Err(tokio::sync::mpsc::error::TryRecvError::Empty)
2726
+ + ));
2727
+ +
2728
+ + chat.finish_cxstatusline_rate_limit_refresh();
2729
+ + assert!(!chat.cxstatusline_rate_limit_pending);
2730
+ + chat.cxstatusline_rate_limit_tick(now + Duration::from_secs(/*secs*/ 59));
2731
+ + assert!(matches!(
2732
+ + rx.try_recv(),
2733
+ + Err(tokio::sync::mpsc::error::TryRecvError::Empty)
2734
+ + ));
2735
+ +
2736
+ + chat.cxstatusline_rate_limit_tick(now + Duration::from_secs(/*secs*/ 60));
2737
+ + assert!(matches!(
2738
+ + rx.try_recv(),
2739
+ + Ok(AppEvent::RefreshRateLimits {
2740
+ + origin: RateLimitRefreshOrigin::CxStatusLine,
2741
+ + })
2742
+ + ));
2743
+ +}
2744
+ +
2745
+ fn enable_test_ambient_pet(chat: &mut ChatWidget) {
2746
+ chat.set_pet_image_support_for_tests(crate::pets::PetImageSupport::Supported(
2747
+ crate::pets::ImageProtocol::Kitty,
2748
+ diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs
2749
+ index 5a196d614a..2a1e758644 100644
2750
+ --- a/codex-rs/tui/src/main.rs
2751
+ +++ b/codex-rs/tui/src/main.rs
2752
+ @@ -1,3 +1,4 @@
2753
+ +#![recursion_limit = "256"]
2754
+ use clap::Parser;
2755
+ use codex_arg0::Arg0DispatchPaths;
2756
+ use codex_arg0::arg0_dispatch_or_else;
2757
+ diff --git a/codex-rs/tui/src/status/rate_limits.rs b/codex-rs/tui/src/status/rate_limits.rs
2758
+ index 2099647d7b..a2eeb92dbe 100644
2759
+ --- a/codex-rs/tui/src/status/rate_limits.rs
2760
+ +++ b/codex-rs/tui/src/status/rate_limits.rs
2761
+ @@ -71,6 +71,8 @@ pub(crate) struct RateLimitWindowDisplay {
2762
+ pub used_percent: f64,
2763
+ /// Human-readable local reset time.
2764
+ pub resets_at: Option<String>,
2765
+ + /// cxstatusline: raw reset time, seconds since the Unix epoch, when the server provided one.
2766
+ + pub resets_at_unix: Option<i64>,
2767
+ /// Window length in minutes when provided by the server.
2768
+ pub window_minutes: Option<i64>,
2769
+ }
2770
+ @@ -86,6 +88,7 @@ impl RateLimitWindowDisplay {
2771
+ Self {
2772
+ used_percent: f64::from(window.used_percent),
2773
+ resets_at,
2774
+ + resets_at_unix: window.resets_at,
2775
+ window_minutes: window.window_duration_mins,
2776
+ }
2777
+ }
2778
+ @@ -447,6 +450,7 @@ mod tests {
2779
+ RateLimitWindowDisplay {
2780
+ used_percent,
2781
+ resets_at: Some("soon".to_string()),
2782
+ + resets_at_unix: None,
2783
+ window_minutes: Some(300),
2784
+ }
2785
+ }
2786
+ @@ -509,11 +513,13 @@ mod tests {
2787
+ primary: Some(RateLimitWindowDisplay {
2788
+ used_percent: 20.0,
2789
+ resets_at: Some("soon".to_string()),
2790
+ + resets_at_unix: None,
2791
+ window_minutes: Some(60),
2792
+ }),
2793
+ secondary: Some(RateLimitWindowDisplay {
2794
+ used_percent: 40.0,
2795
+ resets_at: Some("later".to_string()),
2796
+ + resets_at_unix: None,
2797
+ window_minutes: Some(2 * 60),
2798
+ }),
2799
+ credits: None,
2800
+ diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs
2801
+ index e644dcb2cc..7551933384 100644
2802
+ --- a/codex-rs/tui/src/status/tests.rs
2803
+ +++ b/codex-rs/tui/src/status/tests.rs
2804
+ @@ -69,6 +69,7 @@ fn stale_monthly_limit_marks_fresh_rolling_snapshot_stale() {
2805
+ primary: Some(RateLimitWindowDisplay {
2806
+ used_percent: 20.0,
2807
+ resets_at: Some("soon".to_string()),
2808
+ + resets_at_unix: None,
2809
+ window_minutes: Some(300),
2810
+ }),
2811
+ secondary: None,
2812
+ @@ -1785,11 +1786,13 @@ async fn transcript_overlay_remeasures_status_after_rate_limit_refresh() {
2813
+ primary: Some(RateLimitWindowDisplay {
2814
+ used_percent: 45.0,
2815
+ resets_at: Some("soon".to_string()),
2816
+ + resets_at_unix: None,
2817
+ window_minutes: Some(300),
2818
+ }),
2819
+ secondary: Some(RateLimitWindowDisplay {
2820
+ used_percent: 30.0,
2821
+ resets_at: Some("later".to_string()),
2822
+ + resets_at_unix: None,
2823
+ window_minutes: Some(10_080),
2824
+ }),
2825
+ credits: None,