gwk-tui 0.0.3

The GridWork terminal console — the thin client that renders kernel projections
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
//! The Queue lens — the workday screen.
//!
//! One ranked list of everything asking for the operator: open gates, raised
//! attention, arriving messages, and a recent section for what was closed.
//! The loudest element states the all-clear (the verdict line at row zero),
//! resolved items demote to `recent` rather than gray in place, and absence
//! renders as words, never blank cells.
//!
//! # Gates land here, and nowhere else
//!
//! A relayed permission prompt renders as Queue rows plus the status-bar
//! count. Nothing ever seizes the focused pane: a TUI has no scrim, and the
//! accepted tradeoff is that a genuinely urgent gate waits one keystroke
//! away. A gate that is already decided renders as a **typed result** —
//! the kernel has no gate expiry and `decide_gate` applies whether or not
//! anything still listens, so a prompt shown as answerable after its
//! decision would be a stale prompt over a dead attempt.
//!
//! # Rank
//!
//! `priority` ascending — 0 first, the P0 idiom — with unranked after ranked
//! and byte-ordered ids breaking ties, which is the same order the wire's
//! `COLLATE "C"` pagination walks (opx-01). Rank is presentation only:
//! cost-of-waiting is explicitly deferred by the contract, and this lens does
//! not fake it client-side.
//!
//! # Quiet is not closed
//!
//! An acked or muted row stays on screen with its stamp said in words, and
//! neither counts toward the verdict line or the status bar — quieting is
//! one act with two halves, so the count and the `!` marks always agree.
//! Both leave `resolved_at` absent — the item still holds its dedup slot —
//! and only resolve re-arms: the `recent` header carries that sentence
//! because the demotion is exactly where an operator would otherwise assume
//! the problem cannot come back.

use gwk_domain::command::KernelCommand;
use gwk_domain::entity::{AttentionItem, Gate, Message, Receipt};
use gwk_domain::fsm::{GateVerdict, MessageState};
use gwk_domain::ids::{AttentionItemId, GateId, Seq, Timestamp};
use gwk_theme::marks::{GlyphSet, StateBinding};
use gwk_theme::tier::ColorTier;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};

use crate::input::HitMap;
use crate::theme;
use crate::theme::binding;

/// Everything the lens paints, assembled by the caller from projection pages.
///
/// The lens never fetches: it is a pure function of this value, which is what
/// makes every frame below testable without a kernel.
#[derive(Debug, Clone)]
pub struct QueueState {
    pub attention: Vec<AttentionItem>,
    /// Prompt gates only reach rows (`question` present); the rest of the
    /// gate table belongs to other surfaces.
    pub gates: Vec<Gate>,
    /// The authority audit trail, for the occurrence count on `authority`
    /// rows. Receipts are the ledger of every page; the attention item dedups.
    pub receipts: Vec<Receipt>,
    /// Arrived inter-party messages, newest first. Their presence in this
    /// list IS the non-motion representation of arrival: with motion off (or
    /// absent entirely) the fact still has a row.
    pub messages: Vec<Message>,
    /// The projector's as-of stamp from the page this state was read at.
    pub watermark: Option<Seq>,
    /// The clock the caller read when assembling the state. Mute deadlines
    /// compare against this, never against a clock the render reads itself —
    /// a frame is a value, and a value does not tell time.
    ///
    /// Must be the kernel's own stamp form: UTC, `Z`-suffixed RFC 3339, the
    /// one shape the store pins (`SET TIME ZONE 'UTC'`). `Timestamp`'s byte
    /// order is instant order only within that single form — a local-offset
    /// stamp here would shift every mute comparison by the offset.
    pub now: Timestamp,
}

/// What a Queue row stands for — the target a click or a keystroke acts on.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueueTarget {
    Attention(AttentionItemId),
    Gate(GateId),
}

/// Every actionable target in deterministic visual order, including targets
/// currently outside a short frame's visible window.
pub fn target_order(state: &QueueState) -> Vec<QueueTarget> {
    rows(state, ColorTier::Mono)
        .into_iter()
        .filter_map(|row| row.target)
        .collect()
}

/// How many message rows the mail section shows. Arrival is the fact being
/// represented; history is the drill-down's job.
const MAIL_BUDGET: usize = 3;

/// Stamp the selected attention item seen.
pub fn ack(target: &QueueTarget) -> Option<KernelCommand> {
    match target {
        QueueTarget::Attention(id) => Some(KernelCommand::AckAttention {
            attention_item_id: id.clone(),
        }),
        // A gate is decided, never acked — quieting a question nobody
        // answered would be the stale prompt this lens exists to refuse.
        QueueTarget::Gate(_) => None,
    }
}

/// Quiet the selected attention item until `until`. The deadline is the
/// caller's: the lens has no clock and no opinion on how long quiet lasts.
pub fn mute(target: &QueueTarget, until: Timestamp) -> Option<KernelCommand> {
    match target {
        QueueTarget::Attention(id) => Some(KernelCommand::MuteAttention {
            attention_item_id: id.clone(),
            muted_until: until,
        }),
        QueueTarget::Gate(_) => None,
    }
}

/// Make the selected attention item audible again.
///
/// The mirror of [`mute`], and it exists because the contract has the verb:
/// an item quieted until next week and fixed this afternoon otherwise stays
/// silent with no way back short of resolving it, which would close an item
/// nobody closed.
pub fn unmute(target: &QueueTarget) -> Option<KernelCommand> {
    match target {
        QueueTarget::Attention(id) => Some(KernelCommand::UnmuteAttention {
            attention_item_id: id.clone(),
        }),
        QueueTarget::Gate(_) => None,
    }
}

/// Close the selected attention item.
///
/// The only act that frees the `(kind, subject_ref)` dedup slot, which is why
/// it is a separate verb rather than a third flavour of quiet: ack and mute
/// leave the item open on purpose, so the same problem cannot raise itself a
/// second time while it is still the problem.
pub fn resolve(target: &QueueTarget, resolution: Option<String>) -> Option<KernelCommand> {
    match target {
        QueueTarget::Attention(id) => Some(KernelCommand::ResolveAttention {
            attention_item_id: id.clone(),
            resolution,
        }),
        // A gate is decided, never resolved — the same reason ack refuses it.
        QueueTarget::Gate(_) => None,
    }
}

/// Build the typed decision for an option whose semantics can be stated using
/// the gate's closed verdict vocabulary. Unknown option words are refused
/// rather than silently treating every choice as a pass.
pub fn decide(gate: &Gate, option: &str) -> Option<KernelCommand> {
    if gate.verdict != GateVerdict::Pending
        || !gate
            .options
            .as_ref()
            .is_some_and(|options| options.iter().any(|candidate| candidate == option))
    {
        return None;
    }
    Some(KernelCommand::DecideGate {
        gate_id: gate.id.clone(),
        expected_version: gate.version,
        verdict: verdict_for_option(option)?,
        chosen_option: Some(option.to_owned()),
        evidence_ref: None,
    })
}

pub fn verdict_for_option(option: &str) -> Option<GateVerdict> {
    match option.trim().to_ascii_lowercase().as_str() {
        "accept" | "acceptforsession" | "allow" | "always" | "approve" | "approved"
        | "continue" | "once" | "pass" | "proceed" | "yes" => Some(GateVerdict::Pass),
        "cancel" | "decline" | "deny" | "fail" | "no" | "reject" | "rejected" | "stop" => {
            Some(GateVerdict::Fail)
        }
        "ask" | "defer" | "hold" | "partial" => Some(GateVerdict::Partial),
        _ => None,
    }
}

/// Is this item asking out loud right now?
///
/// Unresolved, not acked, and not muted into the future. Ack and mute quiet
/// the row and the ambient count in one act — the verdict line, the
/// status-bar `!N`, and the set of `!` marks all derive from this predicate,
/// so the frame never states a number its own rows contradict. Both leave
/// the item open: quiet is presentation, resolve is the only close.
///
/// The comparison is byte order over the contract-pinned UTC RFC 3339 form —
/// see [`QueueState::now`] for why that is instant order.
fn audible(item: &AttentionItem, now: &Timestamp) -> bool {
    item.resolved_at.is_none()
        && item.acked_at.is_none()
        && !item.muted_until.as_ref().is_some_and(|until| until > now)
}

/// Unresolved items in presentation order: priority ascending (0 first, the
/// P0 idiom), unranked after ranked, ties and the unranked tail in byte order
/// — the order the wire's `COLLATE "C"` pagination already walks.
fn ranked(items: &[AttentionItem]) -> Vec<&AttentionItem> {
    let mut open: Vec<&AttentionItem> = items.iter().filter(|i| i.resolved_at.is_none()).collect();
    open.sort_by(|a, b| {
        match (a.priority, b.priority) {
            (Some(x), Some(y)) => x.cmp(&y),
            (Some(_), None) => std::cmp::Ordering::Less,
            (None, Some(_)) => std::cmp::Ordering::Greater,
            (None, None) => std::cmp::Ordering::Equal,
        }
        .then_with(|| a.id.as_str().cmp(b.id.as_str()))
    });
    open
}

/// The occurrence trail behind an `authority` row: how many receipts the
/// kernel wrote for this subject, and since when.
///
/// The attention item dedups — one row per live problem — while every page
/// writes a receipt, so the receipts are where "how often" lives. The join is
/// the kernel's own construction: a page's receipt carries the subject as
/// `subject_type`/`subject_id` and its attention item carries the same pair
/// joined as `subject_ref`.
fn occurrences<'a>(
    item: &AttentionItem,
    receipts: &'a [Receipt],
) -> Option<(usize, &'a Timestamp)> {
    if item.kind != "authority" {
        // The count renders on authority rows only (ruled); other kinds have
        // no receipt trail with this shape to count.
        return None;
    }
    let subject_ref = item.subject_ref.as_deref()?;
    let mut count = 0;
    let mut since: Option<&Timestamp> = None;
    for receipt in receipts {
        // The kernel's joined form, matched without re-joining it — no
        // allocation on the render path.
        let matched = subject_ref
            .strip_prefix(receipt.subject_type.as_str())
            .and_then(|rest| rest.strip_prefix('/'))
            == Some(receipt.subject_id.as_str());
        if matched {
            count += 1;
            since = Some(match since {
                Some(s) if s <= &receipt.ts => s,
                _ => &receipt.ts,
            });
        }
    }
    since.map(|s| (count, s))
}

/// `HH:MM` out of an RFC 3339 timestamp, for the compact times rows carry.
fn hhmm(ts: &Timestamp) -> &str {
    ts.as_str().get(11..16).unwrap_or(ts.as_str())
}

/// Is this gate an open prompt — a question still waiting on a human?
///
/// The typed verdict alone decides: `Pending` is open, anything else is
/// decided. `chosen_option` is not consulted, so contradictory wire data — a
/// chosen option beside a `Pending` verdict — stays a prompt rather than
/// rendering the self-contradicting "decided: x (pending)".
fn open_prompt(gate: &Gate) -> bool {
    gate.question.is_some() && gate.verdict == GateVerdict::Pending
}

/// The one count every loud element states: open gates plus audible
/// attention. The verdict line, the status bar, and the set of `!` marks all
/// derive from this single walk, so they cannot drift apart.
fn audible_count(state: &QueueState) -> usize {
    state.gates.iter().filter(|g| open_prompt(g)).count()
        + state
            .attention
            .iter()
            .filter(|i| audible(i, &state.now))
            .count()
}

/// One row's paint: mark cell, styled text, and what it stands for.
struct Row {
    mark: Option<&'static StateBinding>,
    text: String,
    /// A right-aligned tail, painted only when it clears the text — dropped,
    /// not squeezed, in a pane too narrow to hold both.
    right: Option<String>,
    style: Style,
    target: Option<QueueTarget>,
}

impl Row {
    fn plain(text: String, style: Style) -> Self {
        Row {
            mark: None,
            text,
            right: None,
            style,
            target: None,
        }
    }
}

/// Build the frame's rows from the state. Pure, and the whole layout: the
/// painter below only places what this returns.
fn rows(state: &QueueState, tier: ColorTier) -> Vec<Row> {
    let mut out = Vec::new();
    let muted_style = theme::state_style(binding("idle"), tier);
    let warn = binding("needs_attention");
    let done = binding("done");

    let open_gates: Vec<&Gate> = state.gates.iter().filter(|g| open_prompt(g)).collect();
    let count = audible_count(state);

    // The verdict line: the loudest element states the all-clear. Non-empty,
    // it states the count and — right-aligned — the oldest age.
    if count == 0 {
        out.push(Row::plain(
            "all clear -- nothing needs attention".into(),
            theme::state_style(done, tier),
        ));
    } else {
        let oldest = open_gates
            .iter()
            .map(|g| &g.created_at)
            .chain(
                state
                    .attention
                    .iter()
                    .filter(|i| audible(i, &state.now))
                    .map(|i| &i.raised_at),
            )
            .min();
        out.push(Row {
            mark: None,
            text: format!("{count} need attention"),
            right: oldest.map(|t| format!("oldest {}", hhmm(t))),
            style: theme::state_style(warn, tier).add_modifier(Modifier::BOLD),
            target: None,
        });
    }

    // Open gates: the question, then its options one keystroke away.
    for gate in &open_gates {
        let question = gate.question.as_deref().unwrap_or_default();
        let kind = gate.kind.as_deref().unwrap_or("gate");
        out.push(Row {
            mark: Some(warn),
            text: format!("gate {kind}  {question}"),
            right: None,
            style: theme::state_style(warn, tier),
            target: Some(QueueTarget::Gate(gate.id.clone())),
        });
        if let Some(options) = &gate.options {
            out.push(Row::plain(
                format!("   options: {}", options.join(" / ")),
                muted_style,
            ));
        }
    }

    // Attention, ranked. Quiet rows stay visible with their stamp in words:
    // acked and muted are presentation states, and hiding them would read as
    // resolved — the one thing they deliberately are not. The summary owns
    // the left edge — it is the only guaranteed human-readable text, so the
    // quiet decorations sit after it and are what a narrow pane clips first.
    for item in ranked(&state.attention) {
        let mut text = item.summary.clone();
        if let Some((count, since)) = occurrences(item, &state.receipts) {
            text.push_str(&format!("  {count}x since {}", hhmm(since)));
        }
        let live_mute = item
            .muted_until
            .as_ref()
            .filter(|until| **until > state.now);
        let (mark, style) = if live_mute.is_some() || item.acked_at.is_some() {
            (binding("idle"), muted_style)
        } else {
            (warn, theme::state_style(warn, tier))
        };
        if let Some(until) = live_mute {
            text.push_str(&format!("  muted until {}", hhmm(until)));
        } else if item.acked_at.is_some() {
            text.push_str("  acked");
        }
        out.push(Row {
            mark: Some(mark),
            text: format!("{text}  ({})", item.kind),
            right: None,
            style,
            target: Some(QueueTarget::Attention(item.id.clone())),
        });
    }

    // Mail: arrival as standing rows. This is the non-motion representation —
    // with `--motion=off` (or no motion machinery at all) an arrived message
    // is still a fact on the workday screen, not a glint that already faded.
    // Arrived means it: an accepted-but-undelivered or failed message has no
    // arrival to represent, so it gets no arrival row. The stamp is the last
    // state change — for a freshly delivered message, the delivery itself.
    let arrived: Vec<&Message> = state
        .messages
        .iter()
        .filter(|m| {
            matches!(
                m.state,
                MessageState::Delivered | MessageState::Acknowledged | MessageState::Applied
            )
        })
        .collect();
    let failed: Vec<&Message> = state
        .messages
        .iter()
        .filter(|message| {
            matches!(
                message.state,
                MessageState::DeadLetter | MessageState::Rejected
            )
        })
        .collect();
    if arrived.is_empty() && failed.is_empty() {
        out.push(Row::plain("no messages".into(), muted_style));
    } else {
        for message in arrived.iter().take(MAIL_BUDGET) {
            let sender = message.sender.as_deref().unwrap_or("?");
            let recipient = message.recipient.as_deref().unwrap_or("?");
            let kind = message.kind.as_deref().unwrap_or("message");
            out.push(Row::plain(
                format!(
                    "msg {}  {sender} to {recipient}  ({kind})",
                    hhmm(&message.updated_at)
                ),
                muted_style,
            ));
        }
        if arrived.len() > MAIL_BUDGET {
            // The same honesty the body overflow keeps: a cut is named.
            out.push(Row::plain(
                format!("+{} more", arrived.len() - MAIL_BUDGET),
                muted_style,
            ));
        }
        for message in failed.iter().take(MAIL_BUDGET) {
            let sender = message.sender.as_deref().unwrap_or("?");
            let recipient = message.recipient.as_deref().unwrap_or("?");
            let kind = message.kind.as_deref().unwrap_or("message");
            let state_word = match message.state {
                MessageState::DeadLetter => "dead",
                MessageState::Rejected => "rejected",
                _ => "failed",
            };
            out.push(Row {
                mark: Some(warn),
                text: format!(
                    "{state_word} {}  {sender} to {recipient}  ({kind})",
                    hhmm(&message.updated_at)
                ),
                right: None,
                style: theme::state_style(warn, tier),
                target: None,
            });
            let attempts = message.delivery_attempts;
            out.push(Row::plain(
                format!(
                    "   {attempts} attempt{} -- {}",
                    if attempts == 1 { "" } else { "s" },
                    message
                        .dead_letter_reason
                        .as_deref()
                        .unwrap_or("no reason recorded")
                ),
                muted_style,
            ));
        }
        if failed.len() > MAIL_BUDGET {
            out.push(Row::plain(
                format!("+{} more failed deliveries", failed.len() - MAIL_BUDGET),
                muted_style,
            ));
        }
    }

    // Recent: what closed, demoted below the live rows. Resolve is the only
    // verb that frees the dedup slot, so the header says what the demotion
    // means before anyone relies on it.
    out.push(Row::plain(
        "recent -- resolve is re-arm: a recurring problem raises anew".into(),
        muted_style,
    ));
    let mut recent = 0usize;
    for gate in state
        .gates
        .iter()
        .filter(|g| !open_prompt(g) && g.question.is_some())
    {
        // The already-decided prompt as a typed result — never a question
        // still offering its options (opx-02).
        let chosen = gate
            .chosen_option
            .as_deref()
            .unwrap_or("(no option recorded)");
        let verdict = format!("{:?}", gate.verdict).to_lowercase();
        out.push(Row {
            mark: Some(done),
            text: format!("decided: {chosen} ({verdict})"),
            right: None,
            style: muted_style,
            target: None,
        });
        recent += 1;
    }
    for item in state.attention.iter().filter(|i| i.resolved_at.is_some()) {
        let resolution = item.resolution.as_deref().unwrap_or("resolved");
        out.push(Row {
            mark: Some(done),
            text: format!("{}  -- {resolution}", item.summary),
            right: None,
            style: muted_style,
            target: None,
        });
        recent += 1;
    }
    if recent == 0 {
        out.push(Row::plain("   nothing recent".into(), muted_style));
    }

    out
}

/// Paint the Queue into `area`, registering every painted actionable row in
/// `hits`.
///
/// Column zero is the accent column: the selected row carries a reverse-video
/// space there — the tier-independent selection primitive — and at the two
/// tiers that emit colour the row's text additionally takes the `selection`
/// foreground, layered over the row's own style (STM Fork B). The last row is
/// the status bar, always exactly one row (decision 39); a body that
/// overflows names the cut instead of silently truncating. Cut rows are
/// off-frame, not scrolled to: they register no hit and appear in no
/// [`HitMap::targets`] walk, so neither input path can act on a row the
/// operator cannot see.
pub fn render(
    area: Rect,
    buf: &mut Buffer,
    state: &QueueState,
    selected: Option<&QueueTarget>,
    tier: ColorTier,
    glyphs: GlyphSet,
    hits: &mut HitMap<QueueTarget>,
) {
    hits.clear();
    if area.height == 0 || area.width == 0 {
        return;
    }
    let body_rows = area.height - 1;
    let built = rows(state, tier);
    let capacity = body_rows as usize;
    let needs_notice = built.len() > capacity && capacity > 1;
    let visible = capacity
        .saturating_sub(usize::from(needs_notice))
        .min(built.len());
    let selected_row = selected.and_then(|target| {
        built
            .iter()
            .position(|row| row.target.as_ref() == Some(target))
    });
    let start = if visible == 0 {
        0
    } else {
        selected_row
            .filter(|index| *index >= visible)
            .map_or(0, |index| index + 1 - visible)
            .min(built.len().saturating_sub(visible))
    };
    let end = (start + visible).min(built.len());

    let selection_fg = match tier {
        // At ≥256 colours the selected row's foreground shifts to `selection`
        // on top of the accent; at 16/mono the accent cell is the entire
        // signal (STM: the reverse-video space is the carrier at every tier).
        ColorTier::Truecolor | ColorTier::Xterm256 => gwk_theme::SIGNAL
            .iter()
            .find(|t| t.name == "gws_selection")
            .map(|t| theme::token_style(t, tier)),
        ColorTier::Ansi16 | ColorTier::Mono => None,
    };
    // The ratified selection token paints ON TOP of the `>`, never instead
    // of it: reverse video is the expression 16-colour and mono can carry.
    let accent_style = gwk_theme::SIGNAL
        .iter()
        .find(|t| t.name == "gws_selection")
        .map(|t| theme::token_style(t, tier))
        .unwrap_or_default()
        .add_modifier(Modifier::BOLD);

    for (i, row) in built[start..end].iter().enumerate() {
        let y = area.y + i as u16;
        let is_selected = matches!((selected, &row.target), (Some(sel), Some(t)) if *sel == *t);
        let text_style = match (is_selected, selection_fg) {
            // Layered, not replaced: the fg shifts to `selection`, the row's
            // own modifiers stay.
            (true, Some(fg)) => row.style.patch(fg),
            _ => row.style,
        };
        if is_selected {
            buf.set_string(area.x, y, ">", accent_style);
        }
        let mut x = area.x + 1;
        if let Some(state_binding) = row.mark {
            let mark =
                gwk_theme::marks::mark(state_binding.mark).expect("the mark inventory is pinned");
            // Static marks only in this lens; frame 0 is every frame. The
            // mark keeps the row's binding style even on the selected row —
            // the state colour is the mark's meaning — and its write is
            // bounded by the area like every other one.
            let glyph = theme::glyph(mark, 0, glyphs);
            let budget = (area.x + area.width).saturating_sub(x);
            buf.set_stringn(x, y, glyph.to_string(), budget as usize, row.style);
            x += 2;
        }
        let width = (area.x + area.width).saturating_sub(x);
        let safe_text = theme::safe_text(&row.text, width as usize);
        buf.set_stringn(x, y, safe_text.as_ref(), width as usize, text_style);
        if let Some(right) = &row.right {
            crate::row::paint_tail(
                buf,
                area,
                y,
                x.saturating_add(u16::try_from(safe_text.chars().count()).unwrap_or(u16::MAX)),
                right,
                text_style,
            );
        }
        if let Some(target) = &row.target {
            hits.register(Rect::new(area.x, y, area.width, 1), target.clone());
        }
    }

    if needs_notice {
        let y = area.y + body_rows.saturating_sub(1);
        let notice = if start == 0 {
            format!("+{} more", built.len() - end)
        } else if end == built.len() {
            format!("+{start} before")
        } else {
            format!("+{start} before  +{} more", built.len() - end)
        };
        buf.set_stringn(
            area.x + 1,
            y,
            notice,
            area.width.saturating_sub(1) as usize,
            theme::state_style(binding("idle"), tier),
        );
    }

    // The status bar: the ambient half of the gates answer. `!` is the
    // admitted needs-attention glyph — U+26A0 is banned from the cell buffer.
    let as_of = state
        .watermark
        .as_ref()
        .map_or_else(|| "-".to_string(), |w| w.to_string());
    let status = format!("QUEUE  !{}  as-of {as_of}", audible_count(state));
    let safe_status = theme::safe_text(&status, area.width as usize);
    buf.set_stringn(
        area.x,
        area.y + area.height - 1,
        safe_status.as_ref(),
        area.width as usize,
        Style::default(),
    );
}

#[cfg(test)]
mod tests {
    use gwk_domain::envelope::Actor;
    use ratatui::Terminal;
    use ratatui::backend::TestBackend;

    use super::*;

    const NOW: &str = "2026-08-06T12:00:00Z";

    fn ts(s: &str) -> Timestamp {
        Timestamp::new(s)
    }

    fn item(id: &str, summary: &str) -> AttentionItem {
        AttentionItem {
            id: AttentionItemId::new(id),
            kind: "watchdog".into(),
            summary: summary.into(),
            subject_ref: None,
            raised_by: None,
            priority: None,
            raised_at: ts("2026-08-06T09:00:00Z"),
            acked_at: None,
            muted_until: None,
            resolved_at: None,
            resolution: None,
        }
    }

    fn gate(id: &str, question: &str) -> Gate {
        Gate {
            id: GateId::new(id),
            version: 1,
            attempt_id: None,
            phase_ref: None,
            kind: Some("permission".into()),
            question: Some(question.into()),
            options: Some(vec!["allow".into(), "deny".into()]),
            verdict: GateVerdict::Pending,
            chosen_option: None,
            decided_by: None,
            evidence_ref: None,
            created_at: ts("2026-08-06T09:30:00Z"),
            updated_at: ts("2026-08-06T09:30:00Z"),
        }
    }

    fn receipt(subject_type: &str, subject_id: &str, at: &str) -> Receipt {
        Receipt {
            id: gwk_domain::ids::ReceiptId::new(format!("r-{at}")),
            actor: Actor {
                kind: "kernel".into(),
                id: None,
            },
            action: "deploy".into(),
            subject_type: subject_type.into(),
            subject_id: subject_id.into(),
            from: None,
            to: None,
            observed_basis: None,
            ts: ts(at),
        }
    }

    fn empty_state() -> QueueState {
        QueueState {
            attention: Vec::new(),
            gates: Vec::new(),
            receipts: Vec::new(),
            messages: Vec::new(),
            watermark: None,
            now: ts(NOW),
        }
    }

    fn dump_frame(
        w: u16,
        h: u16,
        state: &QueueState,
        selected: Option<&QueueTarget>,
    ) -> (String, HitMap<QueueTarget>, Buffer) {
        dump_frame_tier(w, h, state, selected, ColorTier::Mono)
    }

    fn dump_frame_tier(
        w: u16,
        h: u16,
        state: &QueueState,
        selected: Option<&QueueTarget>,
        tier: ColorTier,
    ) -> (String, HitMap<QueueTarget>, Buffer) {
        let mut terminal = Terminal::new(TestBackend::new(w, h)).expect("terminal");
        let mut hits = HitMap::new();
        terminal
            .draw(|frame| {
                let area = frame.area();
                render(
                    area,
                    frame.buffer_mut(),
                    state,
                    selected,
                    tier,
                    GlyphSet::Unicode,
                    &mut hits,
                );
            })
            .expect("draw");
        let buf = terminal.backend().buffer().clone();
        let mut out = String::new();
        for y in 0..buf.area.height {
            let mut line = String::new();
            for x in 0..buf.area.width {
                line.push_str(buf[(x, y)].symbol());
            }
            out.push_str(line.trim_end());
            out.push('\n');
        }
        (out, hits, buf)
    }

    fn assert_matches_golden(name: &str, rendered: &str) {
        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("goldens")
            .join(format!("{name}.txt"));
        let bless = std::env::var_os("BLESS").is_some();
        if bless {
            if let Some(dir) = path.parent() {
                std::fs::create_dir_all(dir).expect("create goldens dir");
            }
            std::fs::write(&path, rendered).expect("write golden");
        } else {
            let committed = std::fs::read_to_string(&path)
                .unwrap_or_else(|why| panic!("{}: {why} (BLESS=1 to create)", path.display()));
            if committed != rendered {
                let at = committed
                    .lines()
                    .zip(rendered.lines())
                    .position(|(a, b)| a != b);
                panic!(
                    "{} drifted at line {:?}.\n  golden:   {:?}\n  rendered: {:?}\nIf the \
                     frame is right, re-run with BLESS=1.",
                    path.display(),
                    at.map(|l| l + 1),
                    at.and_then(|l| committed.lines().nth(l)),
                    at.and_then(|l| rendered.lines().nth(l)),
                );
            }
        }
        assert!(
            !bless,
            "BLESS=1 rewrites the goldens; it is not a passing run"
        );
    }

    fn workday_state() -> QueueState {
        let mut kek = item("a-kek", "KEK rotation is overdue");
        kek.priority = Some(0);
        let mut deploy = item("a-deploy", "deploy blocked on grant");
        deploy.kind = "authority".into();
        deploy.subject_ref = Some("command/cmd-7".into());
        deploy.priority = Some(1);
        let mut disk = item("a-disk", "disk pressure on the build host");
        disk.acked_at = Some(ts("2026-08-06T10:00:00Z"));
        let mut flap = item("a-flap", "mcp facade flapping");
        flap.muted_until = Some(ts("2026-08-06T18:00:00Z"));
        let mut fixed = item("a-fixed", "runner pool exhausted");
        fixed.resolved_at = Some(ts("2026-08-06T11:00:00Z"));
        fixed.resolution = Some("pool widened".into());

        let mut decided = gate("g-old", "push to main?");
        decided.verdict = GateVerdict::Pass;
        decided.chosen_option = Some("allow".into());

        QueueState {
            attention: vec![disk, kek, flap, deploy, fixed],
            gates: vec![gate("g-1", "rm -rf build/ in worktree?"), decided],
            receipts: vec![
                receipt("command", "cmd-7", "2026-08-06T09:14:00Z"),
                receipt("command", "cmd-7", "2026-08-06T10:20:00Z"),
                receipt("command", "cmd-7", "2026-08-06T11:41:00Z"),
                receipt("command", "other", "2026-08-06T09:00:00Z"),
            ],
            messages: vec![Message {
                id: gwk_domain::ids::MessageId::new("m-1"),
                version: 1,
                state: gwk_domain::fsm::MessageState::Delivered,
                idempotency_key: gwk_domain::ids::IdempotencyKey::new("k-1"),
                correlation_id: None,
                reply_to: None,
                sender: Some("researcher".into()),
                recipient: Some("orchestrator".into()),
                channel: None,
                kind: Some("findings".into()),
                payload: None,
                deadline: None,
                delivery_attempts: 1,
                dead_letter_reason: None,
                delivery_refs: None,
                created_at: ts("2026-08-06T11:58:00Z"),
                updated_at: ts("2026-08-06T11:58:00Z"),
            }],
            watermark: Some(Seq::new(213)),
            now: ts(NOW),
        }
    }

    #[test]
    fn queue_rank_orders_priority_first_then_byte_order_with_unranked_last() {
        let mut b = item("b", "ranked one");
        b.priority = Some(1);
        let mut a = item("a", "ranked zero late id");
        a.priority = Some(0);
        let mut z = item("z", "ranked zero");
        z.priority = Some(0);
        let unranked = item("0-first-by-id", "unranked");
        let items = [b, a, z, unranked];
        let order: Vec<&str> = ranked(&items).iter().map(|i| i.id.as_str()).collect();
        // 0 before 1 (the P0 idiom), byte order inside a rank, and an
        // unranked item last no matter how early its id sorts.
        assert_eq!(order, vec!["a", "z", "b", "0-first-by-id"]);
    }

    #[test]
    fn queue_the_all_none_frame_renders_words_not_blank() {
        let (dump, _, _) = dump_frame(72, 7, &empty_state(), None);
        assert!(
            dump.contains("all clear -- nothing needs attention"),
            "the verdict line states the all-clear:\n{dump}"
        );
        assert!(dump.contains("no messages"), "absence in words:\n{dump}");
        assert!(dump.contains("nothing recent"), "absence in words:\n{dump}");
        assert!(
            dump.contains("as-of -"),
            "an empty log's watermark is said, not skipped:\n{dump}"
        );
        assert_matches_golden("queue-empty", &dump);
    }

    #[test]
    fn queue_a_workday_frame_matches_its_golden() {
        let state = workday_state();
        let selected = QueueTarget::Gate(GateId::new("g-1"));
        let (dump, _, _) = dump_frame(72, 14, &state, Some(&selected));
        assert_matches_golden("queue-workday", &dump);
    }

    #[test]
    fn queue_a_decided_gate_renders_a_typed_result_never_a_stale_prompt() {
        // The seeded negative (opx-02): the kernel has no gate expiry, so the
        // lens is the only thing standing between a decided gate and a prompt
        // that looks answerable. A decided gate offering options fails here.
        let mut decided = gate("g-done", "grant the deploy?");
        decided.verdict = GateVerdict::Pass;
        decided.chosen_option = Some("allow".into());
        let mut state = empty_state();
        state.gates = vec![decided];

        let (dump, hits, _) = dump_frame(72, 10, &state, None);
        assert!(
            dump.contains("decided: allow (pass)"),
            "a decided gate is a typed result:\n{dump}"
        );
        assert!(
            !dump.contains("options:"),
            "a decided gate must not offer options:\n{dump}"
        );
        assert!(
            !dump.contains("gate permission  grant the deploy?"),
            "a decided gate must not render as an open prompt:\n{dump}"
        );
        assert!(
            hits.targets().next().is_none(),
            "a decided gate is not an actionable row"
        );
    }

    #[test]
    fn queue_an_open_gate_never_seizes_the_pane() {
        // P13(a): the gate is rows among rows, not a modal. Everything else
        // still paints in the same frame.
        let mut state = empty_state();
        state.gates = vec![gate("g-1", "proceed?")];
        state.attention = vec![item("a-1", "still visible")];

        let (dump, _, _) = dump_frame(72, 10, &state, None);
        assert!(dump.contains("proceed?"));
        assert!(
            dump.contains("still visible"),
            "an open gate must not displace the rest of the queue:\n{dump}"
        );
    }

    #[test]
    fn queue_the_status_bar_uses_the_admitted_glyph() {
        let mut state = empty_state();
        state.gates = vec![gate("g-1", "proceed?")];
        state.attention = vec![item("a-1", "one more")];

        let (dump, _, _) = dump_frame(72, 10, &state, None);
        assert_eq!(
            dump.lines().nth(9),
            Some("QUEUE  !2  as-of -"),
            "the status bar counts what is audible, in its own cells:\n{dump}"
        );
        assert!(
            !dump.contains('\u{26a0}'),
            "U+26A0 is banned from the cell buffer"
        );
    }

    #[test]
    fn queue_an_authority_row_counts_its_receipts() {
        let state = workday_state();
        let (dump, _, _) = dump_frame(72, 14, &state, None);
        assert!(
            dump.contains("3x since 09:14"),
            "the receipt trail is the occurrence count:\n{dump}"
        );

        // The same receipts against a non-authority row: no count. The rule
        // is display-on-authority-rows-only, not wherever a join happens to
        // match.
        let mut plain = item("a-plain", "not authority");
        plain.subject_ref = Some("command/cmd-7".into());
        assert_eq!(occurrences(&plain, &state.receipts), None);
    }

    #[test]
    fn queue_message_arrival_is_a_standing_row_without_motion() {
        let state = workday_state();
        let (dump, _, _) = dump_frame(72, 14, &state, None);
        assert!(
            dump.contains("msg 11:58  researcher to orchestrator  (findings)"),
            "arrival is a standing row, not a motion event:\n{dump}"
        );
    }

    #[test]
    fn queue_resolved_items_demote_to_recent_under_the_rearm_header() {
        let state = workday_state();
        let (dump, _, _) = dump_frame(72, 14, &state, None);
        let recent_at = dump
            .find("recent -- resolve is re-arm")
            .expect("the recent header says what demotion means");
        let resolved_at = dump
            .find("runner pool exhausted")
            .expect("the resolved item renders");
        assert!(
            resolved_at > recent_at,
            "a resolved item lives below the recent header, not among the live rows"
        );
    }

    #[test]
    fn queue_muted_and_acked_rows_stay_visible_in_words() {
        let state = workday_state();
        let (dump, _, _) = dump_frame(72, 14, &state, None);
        assert!(
            dump.contains("disk pressure on the build host  acked"),
            "an acked row is quiet, not gone, summary first:\n{dump}"
        );
        assert!(
            dump.contains("mcp facade flapping  muted until 18:00"),
            "a muted row says when it comes back, after the summary:\n{dump}"
        );

        // A mute whose deadline has passed is audible again — the client
        // unmutes by time alone, no verb required.
        let mut woke = item("a-woke", "was muted");
        woke.muted_until = Some(ts("2026-08-06T11:00:00Z"));
        assert!(audible(&woke, &ts(NOW)));
    }

    #[test]
    fn queue_selection_is_a_visible_marker_in_column_zero() {
        let mut state = empty_state();
        state.attention = vec![item("a-1", "the row")];
        let target = QueueTarget::Attention(AttentionItemId::new("a-1"));

        let (_, _, buf) = dump_frame(40, 6, &state, Some(&target));
        // The row renders after the verdict line, so it sits at y=1.
        let accent = &buf[(0, 1)];
        assert_eq!(accent.symbol(), ">", "selection survives symbol goldens");
        assert!(
            accent.style().add_modifier.contains(Modifier::BOLD),
            "the marker keeps the focus emphasis"
        );
        let unselected = &buf[(0, 0)];
        assert_eq!(
            unselected.symbol(),
            " ",
            "only selection carries the marker"
        );
    }

    #[test]
    fn queue_clicks_land_on_rows_and_a_miss_stays_a_miss() {
        let mut state = empty_state();
        state.attention = vec![item("a-1", "clickable")];

        let (_, hits, _) = dump_frame(40, 6, &state, None);
        assert_eq!(
            hits.hit(5, 1),
            Some(&QueueTarget::Attention(AttentionItemId::new("a-1"))),
            "the attention row is a click target"
        );
        assert_eq!(hits.hit(5, 0), None, "the verdict line is not actionable");
        assert_eq!(hits.hit(5, 2), None, "one row off is a miss");
    }

    #[test]
    fn queue_ack_and_mute_build_the_typed_commands_and_refuse_gates() {
        let attention = QueueTarget::Attention(AttentionItemId::new("a-1"));
        let gate_target = QueueTarget::Gate(GateId::new("g-1"));

        assert_eq!(
            ack(&attention),
            Some(KernelCommand::AckAttention {
                attention_item_id: AttentionItemId::new("a-1"),
            })
        );
        let until = ts("2026-08-06T13:00:00Z");
        assert_eq!(
            mute(&attention, until.clone()),
            Some(KernelCommand::MuteAttention {
                attention_item_id: AttentionItemId::new("a-1"),
                muted_until: until,
            })
        );
        assert_eq!(
            unmute(&attention),
            Some(KernelCommand::UnmuteAttention {
                attention_item_id: AttentionItemId::new("a-1"),
            })
        );
        assert_eq!(
            resolve(&attention, Some("fixed in #71".into())),
            Some(KernelCommand::ResolveAttention {
                attention_item_id: AttentionItemId::new("a-1"),
                resolution: Some("fixed in #71".into()),
            })
        );
        assert_eq!(
            resolve(&attention, None),
            Some(KernelCommand::ResolveAttention {
                attention_item_id: AttentionItemId::new("a-1"),
                resolution: None,
            }),
            "a close with no words is still a close, never an empty string"
        );
        // A gate is decided, never quieted and never resolved.
        for refused in [
            ack(&gate_target),
            mute(&gate_target, ts(NOW)),
            unmute(&gate_target),
            resolve(&gate_target, None),
        ] {
            assert_eq!(refused, None);
        }
    }

    #[test]
    fn queue_gate_options_map_to_typed_verdicts_without_guessing_unknown_words() {
        let gate = gate("g-1", "ship it?");
        assert_eq!(
            decide(&gate, "allow"),
            Some(KernelCommand::DecideGate {
                gate_id: GateId::new("g-1"),
                expected_version: 1,
                verdict: GateVerdict::Pass,
                chosen_option: Some("allow".into()),
                evidence_ref: None,
            })
        );
        assert_eq!(verdict_for_option("deny"), Some(GateVerdict::Fail));
        assert_eq!(verdict_for_option("hold"), Some(GateVerdict::Partial));
        for option in ["once", "always", "accept", "acceptForSession"] {
            assert_eq!(verdict_for_option(option), Some(GateVerdict::Pass));
        }
        for option in ["reject", "decline", "cancel"] {
            assert_eq!(verdict_for_option(option), Some(GateVerdict::Fail));
        }
        assert_eq!(verdict_for_option("ask"), Some(GateVerdict::Partial));
        assert_eq!(verdict_for_option("restart later"), None);
        assert_eq!(decide(&gate, "not offered"), None);
    }

    #[test]
    fn queue_reaches_every_attention_verb_the_contract_carries() {
        // The lifecycle is four commands and the lens now issues all four.
        // Two of them had no TUI verb and one had no CLI twin; a half-covered
        // lifecycle is how a decommission silently drops an act.
        let attention = QueueTarget::Attention(AttentionItemId::new("a-1"));
        let issued: Vec<&'static str> = [
            ack(&attention),
            mute(&attention, ts(NOW)),
            unmute(&attention),
            resolve(&attention, None),
        ]
        .into_iter()
        .map(|command| command.expect("attention verb").command_type())
        .collect();
        assert_eq!(
            issued,
            [
                "ack_attention",
                "mute_attention",
                "unmute_attention",
                "resolve_attention"
            ]
        );
    }

    #[test]
    fn queue_overflow_names_the_cut() {
        let mut state = empty_state();
        state.attention = (0..12)
            .map(|i| item(&format!("a-{i:02}"), &format!("problem {i}")))
            .collect();

        let (dump, _, _) = dump_frame(40, 6, &state, None);
        // 16 built rows, 4 painted: the notice states the exact cut.
        assert!(
            dump.contains("+12 more"),
            "an overflowing body names the cut instead of silently truncating:\n{dump}"
        );
    }

    #[test]
    fn queue_every_cell_glyph_is_ascii_or_an_admitted_mark() {
        // The admission rule (taste-gate item 5, a hard lock): EAW=Ambiguous
        // codepoints shear frames on ambiguous-wide terminals, so nothing
        // reaches the cell buffer unless it passes the shared admission
        // predicate. This walks the busiest frame so a stray em dash or
        // ellipsis in any row's text fails here, not on an operator's screen.
        let (dump, _, _) = dump_frame(72, 14, &workday_state(), None);
        for ch in dump.chars() {
            assert!(
                ch.is_ascii() || gwk_theme::marks::is_admissible(ch),
                "unadmitted glyph {ch:?} in the cell buffer"
            );
        }
    }

    #[test]
    fn queue_unsafe_wire_glyphs_are_escaped_before_paint() {
        let mut state = empty_state();
        state.attention = vec![item("a-unsafe", "unsafe ◆ 你好 ⚠")];

        let (dump, _, _) = dump_frame(96, 8, &state, None);
        for unsafe_glyph in ['◆', '你', '好', '⚠'] {
            assert!(
                !dump.contains(unsafe_glyph),
                "unsafe glyph {unsafe_glyph:?} reached the buffer:\n{dump}"
            );
        }
        assert!(
            dump.contains("\\u{25C6}"),
            "the value stays retypable:\n{dump}"
        );
        assert!(
            dump.contains("\\u{4F60}"),
            "the value stays retypable:\n{dump}"
        );
    }

    #[test]
    fn queue_ack_quiets_the_count_with_the_row() {
        // Quieting is one act with two halves: the verdict line, the status
        // bar, and the `!` marks must always agree. An acked row that still
        // counted would be a number the frame cannot corroborate.
        let mut seen = item("a-seen", "already seen");
        seen.acked_at = Some(ts("2026-08-06T10:00:00Z"));
        let mut state = empty_state();
        state.attention = vec![seen, item("a-loud", "still loud")];

        let (dump, _, _) = dump_frame(60, 8, &state, None);
        assert!(
            dump.contains("1 need attention"),
            "the acked item leaves the count:\n{dump}"
        );
        assert!(
            dump.contains("!1"),
            "the status bar agrees with the verdict line:\n{dump}"
        );
        assert!(
            dump.contains("already seen  acked"),
            "the acked row is quiet, not gone:\n{dump}"
        );
    }

    #[test]
    fn queue_selection_layers_the_foreground_and_the_mark_keeps_its_colour() {
        // STM Fork B at a colour tier: the selected row's text takes the
        // `selection` foreground LAYERED over its style, and the mark cell
        // keeps the state binding's colour — the state colour is the mark's
        // meaning.
        let mut state = empty_state();
        state.attention = vec![item("a-1", "the row")];
        let target = QueueTarget::Attention(AttentionItemId::new("a-1"));

        let (_, _, buf) = dump_frame_tier(40, 6, &state, Some(&target), ColorTier::Truecolor);
        let warn_fg = theme::state_style(binding("needs_attention"), ColorTier::Truecolor).fg;
        let selection_fg = gwk_theme::SIGNAL
            .iter()
            .find(|t| t.name == "gws_selection")
            .and_then(|t| theme::token_style(t, ColorTier::Truecolor).fg);
        assert!(warn_fg.is_some() && selection_fg.is_some(), "tier emits fg");
        // The row sits at y=1 under the verdict line: accent 0, mark 1, text 3.
        assert_eq!(buf[(1, 1)].style().fg, warn_fg, "the mark keeps its state");
        assert_eq!(
            buf[(3, 1)].style().fg,
            selection_fg,
            "the text takes the selection foreground"
        );
        assert_eq!(
            buf[(0, 1)].style().fg,
            selection_fg,
            "the ratified token paints ON TOP of the accent"
        );

        // At mono the token's only expression is reverse video; a bare BOLD
        // is a weight no ratified row carries at that tier.
        let (_, _, mono) = dump_frame_tier(40, 6, &state, Some(&target), ColorTier::Mono);
        let accent = &mono[(0, 1)];
        assert_eq!(accent.symbol(), ">");
        assert!(
            accent.style().add_modifier.contains(Modifier::REVERSED),
            "the selection token's reverse-video expression rides the accent at mono"
        );
    }

    #[test]
    fn queue_a_narrow_area_stays_inside_its_rect() {
        // The widget paints only the cells it was given: in a split layout a
        // stray write is a scribble on the neighbouring pane.
        let mut state = empty_state();
        state.attention = vec![item("a-1", "a row wider than the pane")];
        let target = QueueTarget::Attention(AttentionItemId::new("a-1"));
        let mut hits = HitMap::new();

        let mut buf = Buffer::empty(Rect::new(0, 0, 40, 6));
        render(
            Rect::new(0, 0, 2, 6),
            &mut buf,
            &state,
            Some(&target),
            ColorTier::Mono,
            GlyphSet::Unicode,
            &mut hits,
        );
        for y in 0..6 {
            for x in 2..40 {
                assert_eq!(
                    buf[(x, y)].symbol(),
                    " ",
                    "painted outside its area at ({x},{y})"
                );
            }
        }

        // Zero width: nothing paints at all.
        let mut empty_buf = Buffer::empty(Rect::new(0, 0, 40, 6));
        render(
            Rect::new(0, 0, 0, 6),
            &mut empty_buf,
            &state,
            Some(&target),
            ColorTier::Mono,
            GlyphSet::Unicode,
            &mut hits,
        );
        assert_eq!(empty_buf, Buffer::empty(Rect::new(0, 0, 40, 6)));
    }
}