a3s 0.10.0

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
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
//! Codex-style terminal UI for the A3S Code agent.
//!
//! Built on the `a3s-tui` TEA framework: it drives an [`AgentSession`] via
//! `session.stream()` and renders the resulting [`AgentEvent`] stream as a live
//! chat transcript, with a scoped approval prompt for tool calls.
//!
//! Streaming bridge: `session.stream()` yields a `tokio::mpsc` receiver. A
//! self-re-issuing "pump" command reads one event, turns it into a `Msg`, and
//! the update handler issues the next pump — feeding the async event stream into
//! the synchronous TEA update loop one event at a time.

use std::collections::{HashMap, HashSet, VecDeque};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use a3s_code_core::config::{CodeConfig, OsConfig};
use a3s_code_core::context::RecentWorkspaceFilesContextProvider;
#[cfg(test)]
use a3s_code_core::dynamic_workflow_store_path;
use a3s_code_core::hitl::TimeoutAction;
use a3s_code_core::llm::{ContentBlock, Message};
use a3s_code_core::workspace::{
    LocalWorkspaceManifest, LocalWorkspaceManifestSnapshot, ManifestWorkspaceBackend,
    WorkspaceServices,
};
use a3s_code_core::{
    Agent, AgentEvent, AgentSession, CodeDiagnosticSeverity, CodeError,
    CodeIntelligenceCapabilities, CodeIntelligenceState, CodeLocation, CodePosition,
    CodeSymbolKind, DocumentSymbol, LocalCodeIntelligence, NavigationKind, SessionOptions,
    SymbolInformation, SystemPromptSlots, ToolCallResult, WorkspaceCodeIntelligence,
};
use a3s_lane::{PriorityItem, PriorityQueue};
use a3s_tui::cmd::{self, Cmd};
use a3s_tui::components::textarea::TextareaMsg;
use a3s_tui::components::viewport::ViewportMsg;
use a3s_tui::components::{
    Alert, AlertKind, DiffLineKind, DiffSpan, InlineAction, Meter, Scrollbar, SessionStatusChip,
    Spinner, Textarea, Toast, ToastKind, Viewport,
};
use a3s_tui::event::{KeyEvent, MouseEvent};
use a3s_tui::keymap::{KeyBinding, Keymap};
use a3s_tui::layout::{Constraint, Layout};
use a3s_tui::style::{Color, Style};
use a3s_tui::{
    AgentChrome, Event, KeyCode, KeyModifiers, Model, ProgramBuilder, Theme as TuiTheme,
};
use tokio::sync::{mpsc, Mutex};

// Team digital assets.
#[path = "assets/clone.rs"]
pub(crate) mod asset_clone;
#[path = "assets/lifecycle.rs"]
pub(crate) mod asset_lifecycle;
use crate::commands::code::naming as asset_naming;

// DeepResearch.
#[path = "deep_research/artifacts.rs"]
mod deep_research_artifacts;
#[path = "deep_research/convergence.rs"]
mod deep_research_convergence;
#[path = "deep_research/evidence_ledger.rs"]
mod deep_research_evidence_ledger;
#[path = "deep_research/host_digest.rs"]
mod deep_research_host_digest;
#[path = "deep_research/host_evidence.rs"]
mod deep_research_host_evidence;
#[path = "deep_research/host_metadata.rs"]
mod deep_research_host_metadata;
#[path = "deep_research/host_prompt.rs"]
mod deep_research_host_prompt;
#[path = "deep_research/host_report.rs"]
mod deep_research_host_report;
#[path = "deep_research/host_workflow.rs"]
mod deep_research_host_workflow;
#[path = "deep_research/inquiry_runtime.rs"]
mod deep_research_inquiry_runtime;
#[path = "deep_research/report_audit.rs"]
mod deep_research_report_audit;
#[path = "deep_research/report_generation.rs"]
mod deep_research_report_generation;
#[cfg(test)]
#[path = "deep_research/report_pipeline_tests.rs"]
mod deep_research_report_pipeline_tests;
#[cfg(test)]
#[path = "deep_research/retrieval_contract_tests.rs"]
mod deep_research_retrieval_contract_tests;
#[cfg(test)]
#[path = "deep_research/retrieval_integration_tests.rs"]
mod deep_research_retrieval_integration_tests;
#[path = "deep_research/sectioned_report.rs"]
mod deep_research_sectioned_report;
#[path = "deep_research/state_journal.rs"]
mod deep_research_state_journal;
#[path = "deep_research/workflow_store.rs"]
mod deep_research_workflow_store;
#[cfg(test)]
pub(crate) use deep_research_artifacts::deep_research_workflow_needs_recovery_report;
#[cfg(test)]
use deep_research_artifacts::looks_like_deep_research_fallback_draft;
#[cfg(test)]
pub(crate) use deep_research_artifacts::materialize_deep_research_fallback_draft;
#[cfg(test)]
use deep_research_artifacts::research_report_artifacts_from_output_for_query;
pub(crate) use deep_research_artifacts::{
    clean_deep_research_final_text_from_artifacts, deep_research_contains_workflow_store_reference,
    deep_research_output_has_internal_leak,
    deep_research_report_rejection_diagnostic_from_answer_text,
    deep_research_workflow_needs_recovery_report_with_metadata,
    materialize_deep_research_completed_report_from_generation,
    materialize_deep_research_recovery_report, parse_embedded_structured_evidence_json,
    research_report_artifacts_from_output, ResearchReportArtifacts,
};
#[cfg(test)]
use deep_research_artifacts::{
    deep_research_completed_report_html_for_test,
    deep_research_report_artifacts_from_output_for_query, deep_research_report_slug,
};
use deep_research_artifacts::{normalize_research_source_anchor, workflow_evidence_summary};
use deep_research_convergence::{
    evaluate_terminal_inquiry_convergence, inquiry_terminal_outcome, validated_inquiry_projection,
    validated_inquiry_publication_outcome, ConvergenceAction, ConvergenceDecision,
    InquiryTerminalOutcome, ValidatedInquiryProjection,
};
use deep_research_evidence_ledger::{accepted_evidence_ledger, AcceptedEvidence};
use deep_research_host_digest::*;
use deep_research_host_evidence::*;
use deep_research_host_metadata::*;
use deep_research_host_prompt::*;
use deep_research_host_report::*;
pub(crate) use deep_research_host_workflow::DeepResearchEvidenceScope;
use deep_research_host_workflow::*;
use deep_research_inquiry_runtime::inquiry_projection_from_workflow;
pub(crate) use deep_research_inquiry_runtime::{
    spawn_deep_research_inquiry, DEEP_RESEARCH_INQUIRY_FINALIZATION_RESERVE_MS,
    DEEP_RESEARCH_INQUIRY_HOST_TIMEOUT_MS, DEEP_RESEARCH_QUESTION_REVIEW_STAGE_TIMEOUT_MS,
    DEEP_RESEARCH_RETRIEVAL_STAGE_TIMEOUT_MS,
};
use deep_research_report_generation::*;
use deep_research_sectioned_report::{
    generate_sectioned_report, merge_sectioned_inquiry_projection, sectioned_report_available,
    SECTIONED_REPORT_BUDGET_MS,
};
#[cfg(test)]
pub(crate) use deep_research_state_journal::load_inquiry_state as deep_research_test_load_inquiry_state;
pub(crate) use deep_research_state_journal::ResearchOutcome;
use deep_research_state_journal::{
    fork_current_for_contradiction_review, reconcile_interrupted_latest_run,
    record_child_event as record_deep_research_child_event,
    record_convergence as record_deep_research_convergence,
    record_evidence_ledger as record_deep_research_evidence_ledger,
    record_inquiry_state as record_deep_research_inquiry_state,
    record_run_terminal as record_deep_research_run_terminal,
    record_workflow_completed as record_deep_research_workflow_completed,
    record_workflow_started as record_deep_research_workflow_started, research_diagnostic,
    research_diff, ResearchDiagnosticKind, ResearchRunProjection, ResearchSpec,
};
#[cfg(test)]
pub(crate) use deep_research_state_journal::{
    record_workflow_started as deep_research_test_record_workflow_started,
    ResearchSpec as DeepResearchTestResearchSpec,
};
pub(crate) use deep_research_workflow_store::{
    ensure_deep_research_workflow_run_id, recover_deep_research_bootstrap_acquisition_from_store,
    recover_deep_research_initial_retrieval_from_store,
    recover_deep_research_workflow_run_from_store,
};

/// Build the same engineered workflow for `a3s code research` that the TUI
/// uses for `?` turns. The CLI must not carry a second planner, workflow
/// source, or timeout policy that can drift from the interactive path.
pub(crate) fn deep_research_cli_workflow_args_for_budget(
    query: &str,
    budget: BudgetPlan,
    evidence_scope: Option<DeepResearchEvidenceScope>,
) -> serde_json::Value {
    let evidence_scope =
        evidence_scope.unwrap_or_else(|| deep_research_inferred_evidence_scope(query));
    deep_research_workflow_args_for_budget(query, evidence_scope, budget)
}

/// Validate the closed evidence boundary for non-interactive report
/// generation. The returned flag records whether the replayed research
/// contract is qualified instead of fully satisfied; unsupported evidence
/// never reaches synthesis.
pub(crate) fn deep_research_cli_report_is_qualified(
    query: &str,
    workflow_output: &str,
    workflow_metadata: Option<&serde_json::Value>,
) -> Result<bool, String> {
    let canonical_output =
        deep_research_canonical_workflow_output(workflow_output, workflow_metadata);
    let evidence_scope = deep_research_inferred_evidence_scope(query);
    let outcome = deep_research_report_outcome_for_workflow(
        query,
        evidence_scope,
        &canonical_output,
        workflow_metadata,
    );
    if matches!(outcome, DeepResearchRunOutcome::Degraded) {
        return Err("evidence collection did not produce a reportable package".to_string());
    }
    let accepted = accepted_evidence_ledger(&canonical_output, workflow_metadata);
    if accepted.is_empty() {
        return Err("evidence collection produced no accepted evidence".to_string());
    }
    Ok(matches!(outcome, DeepResearchRunOutcome::Qualified))
}

#[cfg(test)]
pub(crate) fn deep_research_test_accepted_evidence_ledger(
    workflow_output: &str,
    workflow_metadata: Option<&serde_json::Value>,
) -> Vec<AcceptedEvidence> {
    accepted_evidence_ledger(workflow_output, workflow_metadata)
}

pub(crate) fn deep_research_cli_canonical_workflow_output(
    workflow_output: &str,
    workflow_metadata: Option<&serde_json::Value>,
) -> String {
    deep_research_canonical_workflow_output(workflow_output, workflow_metadata)
}

pub(crate) async fn settle_deep_research_cli_run(
    workspace: &std::path::Path,
    run_id: &str,
    workflow_succeeded: bool,
    workflow_output: &str,
    workflow_metadata: Option<&serde_json::Value>,
    requested_outcome: ResearchOutcome,
    artifacts: &ResearchReportArtifacts,
) -> Result<ResearchOutcome, String> {
    record_deep_research_workflow_completed(workspace, run_id, workflow_succeeded)
        .await
        .map_err(|error| format!("record DeepResearch CLI workflow completion: {error:#}"))?;
    let canonical = deep_research_canonical_workflow_output(workflow_output, workflow_metadata);
    let evidence = accepted_evidence_ledger(&canonical, workflow_metadata);
    record_deep_research_evidence_ledger(workspace, run_id, &evidence)
        .await
        .map_err(|error| format!("record DeepResearch CLI accepted evidence: {error:#}"))?;
    let projection =
        record_deep_research_run_terminal(workspace, run_id, requested_outcome, Some(artifacts))
            .await
            .map_err(|error| format!("record DeepResearch CLI terminal report: {error:#}"))?;
    if !projection.outcome.is_terminal()
        || !projection.active_steps.is_empty()
        || !projection.active_children.is_empty()
    {
        return Err(format!(
            "DeepResearch CLI journal did not settle: outcome={:?}, active_steps={}, active_children={}",
            projection.outcome,
            projection.active_steps.len(),
            projection.active_children.len()
        ));
    }
    Ok(projection.outcome)
}

#[cfg(test)]
pub(crate) async fn deep_research_test_run_status(
    workspace: &std::path::Path,
    run_id: &str,
) -> Result<String, String> {
    research_diagnostic(workspace, Some(run_id), ResearchDiagnosticKind::Status)
        .await
        .map_err(|error| error.to_string())
}

pub(crate) fn deep_research_cli_sectioned_report_available(
    workflow_output: &str,
    workflow_metadata: Option<&serde_json::Value>,
) -> bool {
    sectioned_report_available(workflow_output, workflow_metadata)
}

/// Run the same replayable report pipeline used by the TUI and atomically
/// replace the CLI workflow projection only after it reaches a publishable
/// terminal state.
pub(crate) async fn complete_deep_research_cli_sectioned_report(
    session: &AgentSession,
    query: &str,
    workflow_output: &mut String,
    workflow_metadata: &mut Option<serde_json::Value>,
    run_id: &str,
    timeout_ms: u64,
) -> Result<ToolCallResult, String> {
    if !sectioned_report_available(workflow_output, workflow_metadata.as_ref()) {
        return Err(
            "DeepResearch CLI report synthesis requires an Inquiry in Outlining".to_string(),
        );
    }
    let report_deadline = Instant::now()
        .checked_add(Duration::from_millis(timeout_ms))
        .ok_or_else(|| "DeepResearch CLI report deadline overflowed".to_string())?;
    let generated = generate_sectioned_report(
        session,
        query,
        workflow_output,
        workflow_metadata.as_ref(),
        run_id,
        report_deadline,
    )
    .await?;

    let mut merged_output = workflow_output.clone();
    let mut merged_metadata = workflow_metadata.clone();
    merge_sectioned_inquiry_projection(
        &mut merged_output,
        merged_metadata.as_mut(),
        generated.metadata.as_ref(),
    )?;
    match deep_research_inquiry_publication_outcome(&merged_output, merged_metadata.as_ref())? {
        Some(DeepResearchRunOutcome::Completed | DeepResearchRunOutcome::Qualified) => {}
        Some(outcome) => {
            return Err(format!(
                "DeepResearch CLI report pipeline ended with non-publishable outcome {outcome:?}"
            ));
        }
        None => {
            return Err(
                "DeepResearch CLI report pipeline omitted terminal Inquiry publication authority"
                    .to_string(),
            );
        }
    }

    *workflow_output = merged_output;
    *workflow_metadata = merged_metadata;
    Ok(generated)
}

/// Parse a schema-validated report object and write the Markdown/HTML pair in
/// one host-side operation. The model never writes either long artifact.
pub(crate) fn materialize_deep_research_cli_generated_report(
    workspace: &Path,
    query: &str,
    output: &str,
    exit_code: i32,
    workflow_output: &str,
    workflow_metadata: Option<&serde_json::Value>,
) -> Result<(String, PathBuf, PathBuf), String> {
    let report = deep_research_report_from_generation(output, exit_code)?;
    let artifacts = materialize_deep_research_completed_report_from_generation(
        workspace,
        query,
        &report,
        workflow_output,
        workflow_metadata,
    )?;
    let text = clean_deep_research_final_text_from_artifacts(&artifacts, workspace)
        .unwrap_or(report.markdown);
    Ok((text, artifacts.markdown, artifacts.html))
}

pub(crate) fn materialize_deep_research_cli_recovery_report(
    workspace: &Path,
    query: &str,
    reason: &str,
    workflow_output: &str,
    workflow_metadata: Option<&serde_json::Value>,
) -> Result<(String, PathBuf, PathBuf), String> {
    let artifacts = materialize_deep_research_recovery_report(
        workspace,
        query,
        reason,
        workflow_output,
        workflow_metadata,
    )?;
    let text = clean_deep_research_final_text_from_artifacts(&artifacts, workspace)
        .unwrap_or_else(|| reason.to_string());
    Ok((text, artifacts.markdown, artifacts.html))
}

// System integrations.
#[path = "system/skills.rs"]
pub(crate) mod skills;
#[path = "system/update.rs"]
mod update;

// Local workspace.
#[path = "workspace/git_snapshot.rs"]
mod git_snapshot;
#[path = "workspace/gitutil.rs"]
mod gitutil;

// Local and shared knowledge.
#[path = "knowledge/kbutil.rs"]
pub(crate) mod kbutil;

// Context and memory.
#[path = "context/memutil.rs"]
pub(crate) mod memutil;

// OS Runtime bridge.
#[path = "os/progressive.rs"]
mod os_progressive;
#[path = "os/remote_ui.rs"]
pub(crate) mod remote_ui;
#[path = "os/runtime_policy.rs"]
mod runtime_policy;
mod runtime_projection;
mod transcript;

// Terminal UI support.
#[path = "app/agent_presence.rs"]
mod agent_presence;
#[path = "app/actions.rs"]
mod app_actions;
#[path = "app/async_dispatch.rs"]
mod app_async_dispatch;
#[path = "app/commands.rs"]
mod app_commands;
#[path = "app/events.rs"]
mod app_events;
#[path = "app/fork.rs"]
mod app_fork;
#[path = "app/launch.rs"]
mod app_launch;
#[path = "app/permission_rules.rs"]
mod app_permission_rules;
#[path = "app/permissions.rs"]
mod app_permissions;
#[path = "app/projections.rs"]
mod app_projections;
#[path = "app/research.rs"]
mod app_research;
#[path = "app/research_workflow.rs"]
mod app_research_workflow;
#[path = "app/rewind.rs"]
mod app_rewind;
#[path = "app/runtime.rs"]
mod app_runtime;
#[path = "app/selection.rs"]
mod app_selection;
#[path = "app/session_share.rs"]
mod app_session_share;
#[path = "app/session_state.rs"]
mod app_session_state;
#[path = "app/smoke.rs"]
mod app_smoke;
#[path = "app/submit.rs"]
mod app_submit;
#[path = "app/types.rs"]
mod app_types;
#[path = "app/update.rs"]
mod app_update;
#[path = "app/update_dispatch.rs"]
mod app_update_dispatch;
#[path = "app/view.rs"]
mod app_view;
#[path = "app/workflow_capture.rs"]
mod app_workflow_capture;
#[path = "ui/approval.rs"]
mod approval;
#[path = "ui/attachments.rs"]
mod attachments;
#[path = "ui/batch_view.rs"]
mod batch_view;
#[path = "ui/chrome.rs"]
mod chrome;
#[path = "ui/design_markdown.rs"]
mod design_markdown;
#[path = "ui/editor_state.rs"]
mod editor_state;
#[path = "ui/file_change_view.rs"]
mod file_change_view;
#[path = "ui/image.rs"]
mod image;
#[path = "ui/message_chrome.rs"]
mod message_chrome;
#[path = "ui/plan_review.rs"]
mod plan_review;
#[path = "ui/program_preview.rs"]
mod program_preview;
#[path = "ui/render.rs"]
mod render;
#[path = "ui/syntax.rs"]
mod syntax;
#[path = "ui/tool_style.rs"]
mod tool_style;
#[path = "ui/tool_transcript_view.rs"]
mod tool_transcript_view;
#[path = "ui/util.rs"]
mod util;
use agent_presence::{agent_presence_tick, AgentIslandLaunchOutcome};

pub(crate) mod panels;
#[cfg(test)]
use crate::budget::AUTO_COMPACT_THRESHOLD;
use crate::budget::{
    budget_plan_for_effort_index, context_limit_for_model, effort_uses_automatic_delegation,
    resolve_ctx_limit, BudgetPlan, BudgetWorkload, DEFAULT_TUI_EFFORT_INDEX, EFFORT_LEVELS,
    ULTRACODE_INDEX as ULTRACODE,
};
use crate::config::*;
use app_commands::*;
#[cfg(test)]
use app_launch::resumed_transcript_entries;
pub(crate) use app_launch::{resolve_tui_session_store_dir, run_in};
use app_permission_rules::*;
use app_permissions::*;
use app_projections::*;
pub(crate) use app_session_state::tui_session_state_path;
use app_session_state::*;
use app_smoke::run_smoke;
#[cfg(test)]
use app_smoke::{
    deep_research_smoke_execution_deadline, deep_research_smoke_exhausted_phase_message,
    deep_research_smoke_finalization_phase_deadline, deep_research_smoke_phase_deadline,
    deep_research_smoke_remaining_budget, deep_research_smoke_run_deadline,
    finalize_deep_research_smoke_journal, run_deep_research_smoke_artifact_step,
};
use app_types::*;
use app_update::*;
use app_workflow_capture::*;
use approval::{ApprovalPrompt, ApprovalPromptMsg};
use asset_naming::*;
use attachments::*;
use chrome::*;
use design_markdown::StreamingMarkdown;
use editor_state::*;
use git_snapshot::*;
use gitutil::*;
use image::*;
use memutil::*;
use message_chrome::*;
pub(crate) use panels::ctx::{parse_ctx_search, strip_controls};
pub(crate) use panels::loop_engineering;
use panels::transcript::{SemanticTranscriptViewport, TranscriptViewportAction};
use plan_review::*;
use render::*;
use runtime_policy::RuntimePolicy;
use runtime_projection::{
    CompletedSubagent, CompletedTool, RuntimeProjection, RuntimeToolCheckpoint, SubagentOutcome,
    ToolCallState,
};
use skills::*;
use syntax::*;
use transcript::{
    join_transcript_blocks, transcript_block_separator, Transcript, TranscriptAnchor,
    TranscriptEntry, TranscriptEntryId, TranscriptPoint, TranscriptSelection,
};
use update::*;
use util::*;

const HITL_CONFIRM_TIMEOUT_MS: u64 = 60 * 60 * 1000;
const BACKGROUND_CONFIRM_TIMEOUT_MS: u64 = 500;
const AUTO_REVIEW_IDLE: Duration = Duration::from_secs(300);
const TOOL_EXEC_TIMEOUT_MS: u64 = 30 * 60 * 1000;
const DEEP_RESEARCH_SMOKE_FINALIZATION_RESERVE_MS: u64 = 10_000;
pub(crate) const DEEP_RESEARCH_SECTIONED_SYNTHESIS_TIMEOUT_MS: u64 = SECTIONED_REPORT_BUDGET_MS;
const DEEP_RESEARCH_ABORT_GRACE_MS: u64 = 2_000;
// Planning/retrieval/closed-evidence assessment and the one durable completed-
// report transaction keep independent active-work clocks. A report resume
// consumes the original transaction deadline rather than adding another
// sectioned-report budget.
const DEEP_RESEARCH_RUN_HARD_TIMEOUT_MS: u64 = DEEP_RESEARCH_INQUIRY_HOST_TIMEOUT_MS
    + DEEP_RESEARCH_SECTIONED_SYNTHESIS_TIMEOUT_MS
    + (2 * DEEP_RESEARCH_ABORT_GRACE_MS)
    + DEEP_RESEARCH_SMOKE_FINALIZATION_RESERVE_MS;
const STREAM_START_TIMEOUT_MS: u64 = 10_000;
const STREAM_JOIN_SETTLE_GRACE_MS: u64 = 2_000;
const GRACEFUL_QUIT_STREAM_GRACE_MS: u64 = 2_000;
const GRACEFUL_QUIT_ABORT_SETTLE_MS: u64 = 250;
const GRACEFUL_QUIT_AGENT_PRESENCE_GRACE_MS: u64 = 500;
const GRACEFUL_QUIT_SESSION_CLOSE_GRACE_MS: u64 = 8_000;
const QUEUE_ADMISSION_RETRY_BASE_MS: u64 = 40;
const QUEUE_ADMISSION_RETRY_MAX_MS: u64 = 500;
const TUI_DUPLICATE_TOOL_CALL_THRESHOLD: u32 = 12;
#[allow(dead_code)]
const RESUME_TIMELINE_PAGE_LIMIT: usize = 200;

struct App {
    session: Arc<AgentSession>,
    active_session: SharedActiveSession,
    /// Live projection of independently managed A3S Use MCP and Skill
    /// extensions into the current Code session.
    use_registry: Option<crate::use_registry::UseRegistryHandle>,
    /// Agent + session-rebuild bits, kept so `/model` can switch models by
    /// resuming the session under a new model (no in-place model setter exists).
    agent: Arc<Agent>,
    store: Arc<dyn a3s_code_core::store::SessionStore>,
    confirmation: a3s_code_core::hitl::ConfirmationPolicy,
    deep_research_report_tool_gate: DeepResearchReportToolGate,
    /// This session's id (for model-switch resume + the exit hint).
    session_id: String,
    /// Credential source paired with `model`, persisted per session so a
    /// resumed conversation does not inherit another session's last picker tab.
    model_source: ModelSelectionSource,
    /// Monotonic identity and active request guard for async session rebuilds.
    /// Late results must never replace a newer active session.
    session_rebuild_seq: u64,
    session_rebuild_pending: Option<u64>,
    /// "provider/model" ids from the config, for the /model picker.
    models: Vec<String>,
    /// Context-window size per model id, for the ctx% indicator.
    model_ctx: std::collections::HashMap<String, u32>,
    /// Context window of the active model (0 = unknown).
    context_limit: u32,
    /// Prompt tokens of the last turn = current context fill.
    last_prompt_tokens: usize,
    /// Summary of earlier conversation after a manual `/compact` (reseed).
    compact_summary: Option<String>,
    /// Highest context-fill tier already warned about (0 / 70 / 85), so each
    /// warning prints once per fill-up and re-arms when usage drops back.
    ctx_warned_tier: u8,
    /// Selected index in the /model panel; `Some` means the panel is open.
    model_menu: Option<usize>,
    /// Active tab in the /model panel (0 = config; account tabs when signed in).
    model_tab: usize,
    /// `/relay` session picker and its stale-result guard.
    relay_panel: Option<panels::relay::RelayPanel>,
    relay_scan_seq: u64,
    /// `/tasks` / Ctrl+B delegated-work inspector and cancellation surface.
    task_panel: Option<panels::tasks::TaskPanel>,
    task_panel_seq: u64,
    /// `/permissions` exact session/project grant inspector and revocation surface.
    permission_panel: Option<panels::permissions::PermissionPanel>,
    /// Picker-visible models advertised for the current Codex login.
    codex_account_models: Vec<crate::account_providers::codex::CodexModel>,
    /// Guards the asynchronous Codex catalog refresh from duplicate commands.
    codex_models_loading: bool,
    /// Last successful account catalog refresh; refreshed again after Codex's
    /// five-minute cache window so long-running TUIs see new model rollouts.
    codex_models_refreshed_at: Option<Instant>,
    /// Lazily discovered model ids for account-backed CLI providers.
    account_models: HashMap<crate::account_providers::AccountProvider, Vec<String>>,
    account_models_loading: HashSet<crate::account_providers::AccountProvider>,
    account_model_errors: HashMap<crate::account_providers::AccountProvider, String>,
    /// Custom LLM client to inject for signed-in account tabs; None uses config.acl.
    llm_override: Option<LlmOverride>,
    /// Parsed config used to rebuild config-backed model clients with the same
    /// v5.2 provider capabilities after /model and /effort changes.
    code_config: Arc<CodeConfig>,
    /// Paths resolved once from the immutable CLI invocation and effective ACL.
    asset_directories: crate::commands::config::CodeAssetDirectories,
    config_path: PathBuf,
    memory_dir: PathBuf,
    auto_compact_threshold: f64,
    /// Optional OS endpoint from config.acl; enables /login and /logout.
    os_config: Option<OsConfig>,
    /// Restored OS login (from `~/.a3s/os-auth.json`, persisted across runs);
    /// `None` = signed out. Loaded on startup, set by /login, cleared by /logout.
    os_session: Option<crate::a3s_os::StoredOsSession>,
    /// True while an OS access-token refresh is in flight (guards the BannerTick
    /// trigger from spawning a second refresh before the first resolves).
    os_refreshing: bool,
    /// OS unified-gateway models for the `/model` picker, lazily fetched when
    /// the signed-in user opens the OS Gateway tab. `None` = not fetched yet or
    /// currently loading; `Some([])` = the gateway is unavailable/unconfigured.
    os_gateway_models: Option<Vec<String>>,
    /// True while the OS Gateway tab is fetching its model list. Guards against
    /// spawning duplicate slow requests while the user switches tabs repeatedly.
    os_gateway_models_loading: bool,
    /// The precise reason the last gateway-models fetch failed (e.g. `/v1` not
    /// proxied → HTML, auth error, unreachable), shown in the `/model` picker.
    os_gateway_error: Option<String>,
    /// Last OS view seen in a tool result. Generic tool views are opened by
    /// clicking the inline "Open view" button; owned workflows like `/flow` may
    /// also open their prepared designer view directly.
    last_view: Option<remote_ui::ViewSpec>,
    /// Completed DeepResearch report view captured before settlement. It opens
    /// only when DeepResearch actually finishes.
    pending_deep_research_report_view: Option<remote_ui::ViewSpec>,
    /// Transient state for the active coverage-driven DeepResearch run.
    deep_research_loop: Option<DeepResearchLoop>,
    /// One durable resume of the same sectioned report transaction is allowed
    /// after a pre-publication pipeline failure.
    deep_research_report_resume_used: bool,
    /// Transient host hand-off data. Event-derived lifecycle and quality state
    /// deliberately remain outside this snapshot.
    deep_research_workflow: DeepResearchWorkflowSnapshot,
    /// Terminal classification for the active DeepResearch run. Recovery
    /// artifacts are useful diagnostics but must never be counted as a
    /// completed report.
    deep_research_outcome: DeepResearchRunOutcome,
    /// One-shot durable report resume after the sectioned pipeline fails
    /// before publication. It has priority over generic `/loop` continuation.
    pending_deep_research_report_resume: bool,
    /// Monotonic guard for DeepResearch stream watchdogs; stale timeout ticks
    /// must not affect later turns.
    deep_research_stream_timeout_token: u64,
    /// Monotonic identity for asynchronously-started model streams. A late
    /// StreamStarted/StreamError from a cancelled turn must never replace the
    /// receiver of a queued successor.
    stream_start_token: u64,
    /// An interrupted turn whose async `session.stream` admission has not
    /// returned yet. A queued successor cannot start until that stale worker is
    /// cancelled and its single-flight lease has been released.
    interrupted_stream_start_token: Option<u64>,
    /// Continuation deferred behind `interrupted_stream_start_token`. This is
    /// populated when interruption cleanup wins the race with stream admission.
    pending_interrupted_continuation: Option<InterruptedContinuation>,
    /// Required Runtime use for the current autonomous workflow, plus observed
    /// evidence from tool/subagent/view events.
    runtime_expectation: Option<RuntimeExpectation>,
    /// Current model effort (index into EFFORT_LEVELS).
    effort: usize,
    /// `/effort` slider panel: temp selection while open.
    effort_panel: Option<usize>,
    /// `/theme` picker: temp theme index while open.
    theme_panel: Option<usize>,
    /// First Ctrl+C arms quit; a second within the window exits.
    quit_armed: Option<Instant>,
    /// True while `/exit` or confirmed Ctrl+C is cancelling the session and
    /// settling its active stream. All late UI events are ignored until
    /// `QuitReady`, so cancellation cannot start an automatic continuation.
    quitting: bool,
    /// Last user activity; drives the inactivity auto-review.
    last_activity: Instant,
    /// Tracks which real conversation revision was reviewed and rejects stale
    /// asynchronous results. UI status lines and navigation keys do not alter it.
    auto_review: AutoReviewTracker,
    /// Shell mode: a leading `!` becomes the prompt, the rest is the command.
    shell_mode: bool,
    /// Deep-research mode: a leading `?` launches the fixed host-managed
    /// semantic-plan, single-retrieval, closed-evidence, assessment, and report
    /// pipeline. Box turns cyan.
    research_mode: bool,
    /// True from an asset-scoped review submit until its report is parsed (or
    /// the run is interrupted/fails). Gates capture_review so a turn that merely
    /// QUOTES an a3s-review block can't open a phantom checklist.
    review_pending: bool,
    /// True from a `/sleep` submit until its report is parsed (or the run is
    /// interrupted/fails). Gates capture_sleep the same way.
    sleep_pending: bool,
    /// Last parsed asset-review report (issues + checkbox state). Survives the
    /// panel closing so a follow-up asset review can reopen it.
    review: Option<panels::review::ReviewState>,
    /// `/flow` DAG picker (login-gated); open when `Some`.
    flow: Option<panels::flow::FlowPanel>,
    /// A `/flow <action>` submitted before a flow was selected; run after selection.
    pending_flow_subcommand: Option<panels::flow::FlowSubcommand>,
    /// `/agent` definition picker; open when `Some`.
    agent_picker: Option<panels::agent::AgentPanel>,
    /// A `/agent <action>` submitted before an agent was active; run after selection.
    pending_agent_subcommand: Option<panels::agent::AgentSubcommand>,
    /// The local agent currently being developed by ordinary user turns.
    agent_dev: Option<panels::agent::AgentDevSession>,
    /// `/mcp` asset selector; open when `Some`.
    mcp_picker: Option<panels::mcp::McpPanel>,
    /// A `/mcp <action>` submitted before an MCP was active; run after selection.
    pending_mcp_subcommand: Option<panels::mcp::McpSubcommand>,
    /// The local MCP asset currently being developed by ordinary user turns.
    mcp_dev: Option<panels::mcp::McpDevSession>,
    /// `/skill` picker; open when `Some`.
    skill_picker: Option<panels::skill::SkillPanel>,
    /// A `/skill <action>` submitted before a skill was active; run after selection.
    pending_skill_subcommand: Option<panels::skill::SkillSubcommand>,
    /// The local skill currently being developed by ordinary user turns.
    skill_dev: Option<panels::skill::SkillDevSession>,
    /// `/okf` OKF package picker; open when `Some`.
    okf_picker: Option<panels::okf::OkfPackagePanel>,
    /// A `/okf <action>` submitted before an OKF package was active; run after selection.
    pending_okf_subcommand: Option<panels::okf::OkfCommand>,
    /// The local OKF package currently being developed by ordinary user turns.
    okf_dev: Option<panels::okf::OkfDevSession>,
    /// Whether the review issue-checklist overlay is showing.
    review_open: bool,
    /// `ctx` CLI detected at startup (past-session history search).
    ctx_ready: bool,
    /// Last `/ctx` search hits, addressable as `/ctx <n>`.
    ctx_hits: Vec<panels::ctx::CtxHit>,
    /// A transcript window staged by `/ctx <n>`, attached (one-shot) to the
    /// next outgoing message.
    pending_ctx: Option<String>,
    /// True for the single `Msg::Submit` the `/loop` mechanism emits to
    /// auto-continue — so on_submit doesn't attach a staged `/ctx` window to
    /// this machine turn.
    loop_continuation: bool,
    /// ALL assistant text of the current turn (across mid-turn tool-call
    /// finalizes, which clear the live streaming buffer). capture_review scans
    /// this when a provider leaves `End.text` empty.
    turn_text: String,
    /// Transaction boundary for one Core LLM turn. Repeating `TurnStart` with
    /// the same number restores this snapshot before an in-place stream retry.
    llm_turn_checkpoint: Option<LlmTurnUiCheckpoint>,
    /// Active transcript text-selection (mouse drag → highlight → copy on
    /// release); `None` when there's no selection.
    selection: Option<Selection>,
    /// Latest dynamic-workflow artifact (ultracode dynamic workflow or task dispatch),
    /// retained for synthesis and shown collapsed in the transcript.
    last_workflow: Option<String>,
    /// Clipboard images pasted into the composer, sent with the owning message.
    pending_images: Vec<PendingImage>,
    /// Persistent north-star goal (`/goal`), prepended to each prompt.
    goal: Option<String>,
    /// When the current `/goal` was set — drives the "Pursuing goal (1h 32m)"
    /// elapsed timer in the status bar. `None` whenever `goal` is `None`.
    goal_since: Option<Instant>,
    /// Durable `/goal` execution state. Unlike `/loop`, this has no turn cap:
    /// only a matching Core GoalAchieved event can close it.
    goal_run: Option<panels::goal_engineering::GoalRunState>,
    /// Incomplete goal retained across TUI exits. It remains paused until the
    /// startup picker or `/goal resume` explicitly activates it.
    paused_goal: Option<PausedGoalState>,
    /// Selected row in the startup "Resume paused goal?" picker.
    goal_resume_prompt: Option<usize>,
    /// Monotonic invalidation token for delayed goal retries.
    goal_generation: u64,
    /// Retry context retained until Core's stream worker releases its
    /// single-flight lease. Goal continuation starts only after that join.
    pending_goal_failure: Option<String>,
    /// User goal temporarily shadowed by an active DeepResearch task.
    deep_research_goal_restore: Option<(Option<String>, Option<Instant>)>,
    /// Remaining auto-continue turns for `/loop` (0 = off).
    loop_remaining: usize,
    /// ECS-style projection of live runtime tool and subagent entities.
    runtime: RuntimeProjection,
    /// Exact local lifecycle publishing and the system-level island bridge.
    /// Rendering belongs to the independent native `a3s-webview` process.
    agent_presence: agent_presence::AgentPresenceRuntime,
    /// Active background completion watchers, keyed by rebuild generation and
    /// task id so session replacement cannot leak stale results into history.
    background_subagent_watches: HashSet<(u64, String)>,
    /// Monotonic identity for asynchronous tracker snapshots. DeepResearch
    /// settlement invalidates older requests before exposing a terminal report.
    subagent_snapshot_request_id: u64,
    deep_research_subagent_settlement_inflight: bool,
    /// Prevent duplicate terminal journal writes while the final projection is
    /// being persisted before the TUI clears its DeepResearch state.
    deep_research_journal_finalization_inflight: bool,
    /// Validated report pair staged for the terminal journal event.
    deep_research_terminal_artifacts: Option<ResearchReportArtifacts>,
    /// Monotonic cursor for normalized `AgentEvent` projections.
    deep_research_agent_event_sequence: u64,
    /// Latest replayable DeepResearch view used by pinned TUI projections.
    deep_research_projection: Option<ResearchRunProjection>,
    /// True once this turn used tools/planning/subagents that need a final
    /// user-facing synthesis if the model stops without text afterwards.
    turn_had_agent_activity: bool,
    /// True once assistant text arrived after the latest tool/planning/subagent
    /// activity in this turn.
    turn_text_after_activity: bool,
    /// Guard for the hidden ultracode continuation that turns raw workflow
    /// results into a final answer.
    ultracode_synthesis_inflight: bool,
    /// At most one hidden synthesis continuation per user turn.
    ultracode_synthesis_used: bool,
    /// Project instructions (CLAUDE.md/AGENT.md), injected into the system prompt.
    instructions: Option<String>,
    /// Shared in-memory workspace file manifest, refreshed by a background watcher.
    workspace_manifest: Arc<LocalWorkspaceManifest>,
    workspace_manifest_rx: SharedManifestRx,
    /// Manifest-backed workspace backend used by agent tools.
    workspace_services: Arc<WorkspaceServices>,
    /// Start of the short brand-gradient input-border flourish after Ultracode
    /// activation; cleared as soon as its dedicated animation finishes.
    gradient_until: Option<Instant>,
    gradient_frame: usize,
    /// Invalidates delayed ticks after cancel/reopen or phase handoff.
    ultracode_animation_epoch: u64,
    /// Ultracode confirm animation playing in the /effort panel before it closes.
    effort_anim: Option<Instant>,
    /// Full-width, style-preserving semantic transcript opened by Ctrl+T.
    transcript_view: Option<SemanticTranscriptViewport>,
    viewport: Viewport,
    textarea: Textarea,
    spinner: Spinner,
    streaming: StreamingMarkdown,
    /// Whether the current turn streamed any text deltas (vs. text only at End).
    got_delta: bool,
    /// Set while `/compact` is summarizing — drives the progress bar + blocks input.
    compacting: Option<Instant>,
    /// Set while `/update` is upgrading — drives a progress bar + blocks input;
    /// on success the app restarts into the new binary.
    updating: Option<Instant>,
    /// Host-owned, read-only `/checkup` preflight. The composer stays hidden
    /// until its typed result is handed to the strict Plan audit.
    checkup_inflight: bool,
    /// Last time the streaming viewport was rebuilt — throttles the O(n) rebuild
    /// to ~30fps so a flood of deltas doesn't starve animation on the 1 loop.
    last_paint: Option<Instant>,
    /// Live reasoning ("thinking") text for the current turn, shown dimmed above
    /// the answer and cleared when the answer is finalized.
    thinking: String,
    state: State,
    messages: Transcript,
    rx: Option<SharedRx>,
    stream_join: Option<StreamJoin>,
    /// True after a terminal event while the stream worker is still releasing
    /// persistence and the core single-flight admission lease. Input remains
    /// queue-only until `StreamJoinSettled` arrives.
    stream_join_settling: bool,
    /// Lets Esc release a terminal stream worker immediately when a queued
    /// follow-up is waiting, without starting the next turn before the lease is
    /// actually dropped.
    stream_settle_abort: Option<tokio::task::AbortHandle>,
    /// Abort handle for host-direct tools such as the DeepResearch workflow.
    host_tool_abort: Option<HostToolAbort>,
    /// True while `rx` is carrying host-direct tool progress rather than an
    /// agent stream; channel close must not finish the turn.
    host_progress_inflight: bool,
    /// Stable call ID emitted by the active host-direct tool lifecycle.
    host_tool_call_id: Option<String>,
    interrupting: bool,
    /// Manual tool approvals waiting for a decision, in request order.
    pending_tools: VecDeque<PendingToolApproval>,
    /// Exact session/project grants shared across model and effort rebuilds.
    permission_grants: TuiPermissionGrants,
    /// Mode-aware permission boundary shared with every rebuilt Core session.
    /// It always tracks the immutable mode of the running turn.
    execution_policy: TuiExecutionPolicy,
    /// Dedicated project ACL file for reviewed persistent grants.
    project_permission_rules_path: PathBuf,
    /// Project rule write currently in flight. Its request remains at the FIFO
    /// head until persistence succeeds, so a failed write cannot silently
    /// broaden the active session.
    permission_rule_write_inflight: Option<String>,
    /// Monotonic identity and global lock for atomic project-grant revocation.
    project_permission_revoke_seq: u64,
    project_permission_revoke_inflight: Option<(u64, ExactPermissionGrant)>,
    /// Denial feedback temporarily owns the composer while retaining its draft.
    approval_feedback: Option<ApprovalFeedback>,
    /// Selected row in the tool-approval options panel.
    approval_sel: usize,
    /// Submitted prompts, oldest first, for ↑/↓ recall.
    history: Vec<String>,
    /// `/history` / Ctrl+R fuzzy prompt-history search.
    history_panel: Option<panels::history::HistoryPanel>,
    /// Cursor into `history` while browsing; `None` means "fresh input".
    history_pos: Option<usize>,
    /// Scratch input captured when prompt-history browsing starts.
    history_draft: Option<String>,
    /// Model name reported by the provider (captured from the first turn).
    model: Option<String>,
    /// Cumulative OUTPUT (generated) tokens this session — what `↓` reports.
    output_tokens: usize,
    /// When the current run started, for the live elapsed-time indicator.
    stream_started: Option<Instant>,
    /// Animation counter for the blinking running-tool dot (advances per tick).
    blink_tick: u8,
    /// Frame counter for the welcome-mascot animation.
    anim: u8,
    /// Run mode (Shift+Tab cycles default → plan → auto).
    mode: Mode,
    /// The mode to restore once an autonomous directive run finishes —
    /// `Some` while such a run auto-switched to `Mode::Auto`.
    autonomy_restore: Option<Mode>,
    /// Host turns submitted while the agent is busy. Ordering and FIFO
    /// semantics come directly from a3s-lane.
    queue: PriorityQueue<Queued>,
    /// Submission-time execution mode keyed by a3s-lane's stable sequence.
    /// Keeping this beside the queue avoids mutable footer state changing the
    /// semantics of a turn that was already submitted.
    queued_turn_modes: HashMap<u64, Mode>,
    /// Strict planning requests keyed by the same stable queue sequence.
    queued_plan_drafts: HashMap<u64, PlanDraftRequest>,
    /// Exact pending turn selected for Send now. This control-plane pointer
    /// overrides normal priority/FIFO ordering without rewriting Lane metadata.
    send_now_queued_sequence: Option<u64>,
    /// `/queue` inspection and control modal.
    queue_panel: Option<panels::queue::QueuePanel>,
    /// Pre-turn state admitted with the active user stream. It becomes a
    /// conflict-safe rewind checkpoint only after Core persistence settles.
    active_rewind_checkpoint: Option<RewindCheckpointSeed>,
    /// Recent completed user turns, newest at the back.
    rewind_checkpoints: VecDeque<RewindCheckpoint>,
    /// Monotonic identity for completed rewind checkpoints.
    next_rewind_checkpoint_id: u64,
    /// Stream token whose post-turn Git snapshot is being finalized.
    rewind_finalization_pending: Option<u64>,
    /// A claimed queued turn remains here until Core admits it. Admission
    /// failure restores this exact item, including its original FIFO sequence.
    active_queued_turn: Option<PriorityItem<Queued>>,
    active_queued_turn_token: Option<u64>,
    /// Immutable mode of the current stream, retained after queue admission.
    active_turn_mode: Option<Mode>,
    /// Planning request owned by the current read-only stream.
    active_plan_draft: Option<PlanDraftRequest>,
    queue_retry_generation: u64,
    queue_retry_attempt: u8,
    /// Text of the message currently being processed (the running task).
    running_task: Option<String>,
    /// Typed live plan/TODO projection, pinned above the input and updated from
    /// PlanningEnd/TaskUpdated or the Codex-compatible `update_plan` tool.
    plan: PlanProjection,
    /// Review staged until the completed stream releases Core's single-flight
    /// lease, then promoted to the modal decision boundary below.
    pending_plan_review: Option<PlanReviewState>,
    /// Explicit Approve / Revise / Abandon boundary for a completed plan.
    plan_review: Option<PlanReviewState>,
    /// `/ide` file-tree + viewer panel (Some when open).
    ide: Option<Ide>,
    /// `/memory` full-screen timeline panel (Some when open).
    memory: Option<MemPanel>,
    /// `/evolution` memory-derived candidate review and asset lifecycle panel.
    evolution: Option<panels::evolution::EvolutionPanel>,
    /// Asset-scoped OS digital-asset browser.
    asset_list: Option<panels::asset_resources::AssetListPanel>,
    /// Asset-scoped OS Runtime activity panel.
    runtime_activity: Option<panels::asset_resources::RuntimeActivityPanel>,
    /// `/kb` full-screen local personal knowledge-base panel (Some when open).
    kb: Option<panels::kb::KbPanel>,
    /// `/loop` engineered loop dashboard (Some when open).
    loop_panel: Option<panels::loop_engineering::LoopPanel>,
    /// `/help` overlay panel is showing.
    help_open: bool,
    /// Scroll offset inside the `/help` overlay.
    help_scroll: usize,
    /// Turns completed this session, for the status-bar task counter.
    completed: usize,
    /// Working directory shown for context.
    cwd: String,
    /// Git branch of the workspace (if any), shown in the bottom status bar.
    branch: Option<String>,
    /// Selected index in the `/` command menu.
    slash_sel: usize,
    /// Exact slash draft whose menu was dismissed with Esc or mouse cancel.
    slash_menu_dismissed_for: Option<String>,
    /// Workspace files (for the `@` file picker) + its selected index.
    files: Vec<String>,
    file_sel: usize,
    /// Expanded directories in the `@` picker tree (collapsed by default).
    at_expanded: std::collections::HashSet<String>,
    /// Count of discoverable Claude skills (incl. plugin-bundled) for the banner.
    skill_count: usize,
    /// Loaded skills (name, description) for the slash menu + `/plugin`.
    skills: Vec<(String, String)>,
    /// Skill names the user disabled via `/plugin` (persisted, hidden from `/`).
    disabled_skills: std::collections::HashSet<String>,
    /// `/plugin` panel: selected row while open.
    plugins_panel: Option<usize>,
    /// Newer release found at startup (latest version), if any.
    update_available: Option<String>,
    width: u16,
    height: u16,
    keymap: Keymap<Action>,
}

impl App {
    fn composer_input_is_hidden(&self) -> bool {
        self.goal_resume_prompt.is_some()
            || self.state == State::Awaiting
            || (self.plan_review.is_some() && !self.plan_review_input_active())
            || self.transcript_view.is_some()
            || self.queue_panel.is_some()
            || self.history_panel.is_some()
            || self.model_menu.is_some()
            || self.relay_panel.is_some()
            || self.task_panel.is_some()
            || self.permission_panel.is_some()
            || self.checkup_inflight
            || self.effort_panel.is_some()
            || self.theme_panel.is_some()
            || self.plugins_panel.is_some()
            || self.review_open
            || self.memory.is_some()
            || self.evolution.is_some()
            || self.asset_list.is_some()
            || self.runtime_activity.is_some()
            || self.kb.is_some()
            || self.loop_panel.is_some()
            || self.flow.is_some()
            || self.agent_picker.is_some()
            || self.mcp_picker.is_some()
            || self.skill_picker.is_some()
            || self.okf_picker.is_some()
            || self.help_open
    }

    fn begin_graceful_quit(&mut self) -> Option<Cmd<Msg>> {
        if self.quitting {
            return None;
        }

        // Checkpoint the resumable UI state immediately. The goal's loop files
        // are marked paused only after the stream has settled, avoiding a race
        // with an in-flight iteration that may still be writing STATE.md.
        if let Err(error) = self.persist_tui_session_state() {
            tracing::warn!(%error, "failed to checkpoint TUI session settings before exit");
        }

        self.quitting = true;
        self.interrupting = true;
        if let Some(ide) = self.ide.as_mut() {
            ide.intelligence_cancellation.cancel();
            ide.intelligence_jump_cancellation.cancel();
        }
        self.stream_start_token = self.stream_start_token.wrapping_add(1);
        self.deep_research_stream_timeout_token =
            self.deep_research_stream_timeout_token.wrapping_add(1);
        self.push_line(&Style::new().fg(TN_YELLOW).render("  exiting…"));

        let session = Arc::clone(&self.session);
        let stream_join = self.stream_join.take();
        let host_tool_abort = self.host_tool_abort.take();
        let agent_presence = self.agent_presence.publisher.clone();
        self.rx = None;

        Some(cmd::cmd(move || async move {
            // Remove the exact heartbeat before potentially waiting on model
            // cleanup. A blocked filesystem must not wedge quit; if bounded
            // removal times out, the normal heartbeat TTL retires the row.
            if tokio::time::timeout(
                Duration::from_millis(GRACEFUL_QUIT_AGENT_PRESENCE_GRACE_MS),
                agent_presence.remove(),
            )
            .await
            .is_err()
            {
                tracing::warn!("timed out removing the local agent-presence heartbeat");
            }
            if let Some(abort) = host_tool_abort {
                abort.abort();
            }

            let close = settle_session_close_for_quit(
                async move {
                    session.close().await;
                },
                Duration::from_millis(GRACEFUL_QUIT_SESSION_CLOSE_GRACE_MS),
            );
            match stream_join {
                Some(stream_join) => {
                    let settle = settle_stream_join_for_quit(
                        stream_join,
                        Duration::from_millis(GRACEFUL_QUIT_STREAM_GRACE_MS),
                    );
                    let _ = tokio::join!(close, settle);
                }
                None => {
                    close.await;
                }
            }

            Msg::QuitReady
        }))
    }

    fn finish_graceful_quit(&mut self) -> Option<Cmd<Msg>> {
        self.pause_goal_for_exit();
        if let Err(error) = self.persist_tui_session_state() {
            tracing::warn!(%error, "failed to finalize TUI session settings before exit");
        }
        Some(cmd::quit())
    }

    fn request_subagent_snapshots(&mut self) -> Cmd<Msg> {
        self.subagent_snapshot_request_id = self.subagent_snapshot_request_id.wrapping_add(1);
        load_subagent_snapshots(
            self.session.clone(),
            self.session_id.clone(),
            self.session_rebuild_seq,
            self.subagent_snapshot_request_id,
        )
    }

    fn invalidate_subagent_snapshots(&mut self) {
        self.subagent_snapshot_request_id = self.subagent_snapshot_request_id.wrapping_add(1);
    }

    pub(crate) fn touch_workspace_file(&self, path: &str) {
        self.workspace_manifest.touch_file(path);
    }

    pub(crate) fn viewport_content_width(&self) -> usize {
        viewport_content_width_for(self.width)
    }

    fn transcript_markdown_width(&self) -> usize {
        transcript_markdown_width_for(self.width)
    }
}

include!("approval_layout.rs");