a3s 0.8.2

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
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
//! 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 an inline (y/n/a) 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::{BinaryHeap, 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, SessionOptions, SystemPromptSlots, ToolCallResult,
};
use a3s_tui::cmd::{self, Cmd};
use a3s_tui::components::textarea::TextareaMsg;
use a3s_tui::components::viewport::ViewportMsg;
use a3s_tui::components::{
    Alert, AlertKind, ChoicePrompt, ChoicePromptItem, ChoicePromptMsg, 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"]
mod asset_clone;
#[path = "assets/lifecycle.rs"]
mod asset_lifecycle;
#[path = "assets/naming.rs"]
mod asset_naming;
#[path = "code_cli.rs"]
mod code_cli;
pub(crate) use code_cli::{is_code_cli_command, run_code_cli};

// DeepResearch.
#[path = "deep_research/artifacts.rs"]
mod deep_research_artifacts;
#[path = "deep_research/convergence.rs"]
mod deep_research_convergence;
#[cfg(test)]
#[path = "deep_research/engineered_loop_tests.rs"]
mod deep_research_engineered_loop_tests;
#[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/prompts.rs"]
mod deep_research_prompts;
#[path = "deep_research/report_audit.rs"]
mod deep_research_report_audit;
#[path = "deep_research/report_phase.rs"]
mod deep_research_report_phase;
#[path = "deep_research/state_journal.rs"]
mod deep_research_state_journal;
#[path = "deep_research/workflow_store.rs"]
mod deep_research_workflow_store;
#[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_artifacts_from_output_for_current_run,
    deep_research_report_artifacts_from_output_for_query, deep_research_report_slug,
    deep_research_workflow_needs_recovery_report,
    materialize_deep_research_completed_report_from_answer_text,
    materialize_deep_research_completed_report_from_markdown,
    materialize_deep_research_completed_report_from_workflow_evidence,
    materialize_deep_research_recovery_report, parse_embedded_structured_evidence_json,
    research_report_artifacts_from_output, research_report_artifacts_from_output_for_current_run,
    snapshot_deep_research_report_artifacts, DeepResearchReportArtifactBaseline,
    ResearchReportArtifacts,
};
use deep_research_artifacts::{normalize_research_source_anchor, workflow_evidence_summary};
use deep_research_convergence::{evaluate_convergence, ConvergenceDecision, ConvergenceInput};
use deep_research_evidence_ledger::{
    accepted_evidence_ledger,
    synthesis_payload_with_context as accepted_evidence_synthesis_payload, 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::*;
use deep_research_host_workflow::*;
use deep_research_report_phase::{
    suppress_tool_output as suppress_deep_research_report_phase_tool_output, ReportPhaseToolBuffer,
};
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_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, ResearchOutcome, ResearchRunProjection, ResearchSpec,
};
pub(crate) use deep_research_workflow_store::{
    ensure_deep_research_workflow_run_id, recover_deep_research_workflow_run_from_store,
};

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

// Local workspace.
#[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"]
mod memutil;

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

// Terminal UI support.
#[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/launch.rs"]
mod app_launch;
#[path = "app/permissions.rs"]
mod app_permissions;
#[path = "app/projections.rs"]
mod app_projections;
#[path = "app/research.rs"]
mod app_research;
#[path = "app/runtime.rs"]
mod app_runtime;
#[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/chrome.rs"]
mod chrome;
#[path = "ui/design_markdown.rs"]
mod design_markdown;
#[path = "ui/editor_state.rs"]
mod editor_state;
#[path = "ui/image.rs"]
mod image;
#[path = "ui/program_preview.rs"]
mod program_preview;
#[path = "ui/render.rs"]
mod render;
#[path = "ui/syntax.rs"]
mod syntax;
#[path = "ui/util.rs"]
mod util;

mod panels;
use crate::budget::{
    budget_plan_for_effort_index, context_limit_for_model, effort_uses_automatic_delegation,
    resolve_ctx_limit, BudgetPlan, BudgetWorkload, AUTO_COMPACT_THRESHOLD,
    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::run;
use app_permissions::*;
use app_projections::*;
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,
    run_deep_research_smoke_artifact_step,
};
use app_types::*;
use app_update::*;
use app_workflow_capture::*;
use asset_naming::*;
use chrome::*;
use design_markdown::StreamingMarkdown;
use editor_state::*;
use gitutil::*;
use image::*;
use memutil::*;
pub(crate) use panels::loop_engineering;
use panels::transcript::{SemanticTranscriptViewport, TranscriptViewportAction};
use render::*;
use runtime_policy::RuntimePolicy;
use runtime_projection::{
    CompletedSubagent, CompletedTool, RuntimeProjection, SubagentOutcome, ToolCallState,
};
use skills::*;
use syntax::*;
use transcript::{Transcript, TranscriptAnchor, TranscriptEntry, TranscriptEntryId};
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_SCRIPT_TIMEOUT_MS: u64 = 300 * 1000;
const DEEP_RESEARCH_WORKFLOW_HOST_GRACE_MS: u64 = 30_000;
// Planning, retrieval, checking, and synthesis keep independent active-work
// clocks. This wall-clock fuse only prevents pathological orchestration from
// escaping the query-agnostic safety envelope.
const DEEP_RESEARCH_RUN_HARD_TIMEOUT_MS: u64 = 6 * 60 * 1000;
const DEEP_RESEARCH_SMOKE_FINALIZATION_RESERVE_MS: u64 = 10_000;
const DEEP_RESEARCH_SYNTHESIS_TIMEOUT_MS: u64 = 90 * 1000;
const DEEP_RESEARCH_REPAIR_TIMEOUT_MS: u64 = 90 * 1000;
const DEEP_RESEARCH_ABORT_GRACE_MS: u64 = 2_000;
const GRACEFUL_QUIT_STREAM_GRACE_MS: u64 = 2_000;
const GRACEFUL_QUIT_ABORT_SETTLE_MS: u64 = 250;
const DEEP_RESEARCH_TOOL_COMPLETION_GRACE_MS: u64 = 15_000;
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,
    /// 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,
    /// 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,
    /// 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>,
    /// 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 all verification
    /// layers have drained. It opens only when DeepResearch actually finishes.
    pending_deep_research_report_view: Option<remote_ui::ViewSpec>,
    /// Bounded DeepResearch verification state; turns generic loop continuation
    /// into report-focused gap checks instead of another broad planning round.
    deep_research_loop: Option<DeepResearchLoop>,
    /// One extra repair pass is allowed when synthesis misses the required local
    /// report marker/artifact, including "single synthesis pass" research.
    deep_research_report_repair_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 prompt generated when the active DeepResearch synthesis missed
    /// its report artifacts. It has priority over generic verification loops.
    pending_deep_research_report_repair_prompt: Option<String>,
    /// 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,
    /// 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 `?` turns the input into a deep-research
    /// query — sent to the agent with a multi-source research directive. 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,
    /// 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 (Ctrl+V), sent with the next message.
    pending_images: Vec<a3s_code_core::llm::Attachment>,
    /// 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>,
    /// 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,
    /// 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,
    deep_research_report_tools: ReportPhaseToolBuffer,
    /// 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>,
    /// 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,
    /// 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<(String, String)>,
    /// Selected row in the tool-approval options panel (0 yes · 1 always · 2 no).
    approval_sel: usize,
    /// Submitted prompts, oldest first, for ↑/↓ recall.
    history: Vec<String>,
    /// 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>,
    /// User messages submitted while the agent is busy, run when it frees up.
    queue: BinaryHeap<Queued>,
    /// Monotonic counter for FIFO ordering within a queue priority.
    seq: u64,
    /// 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,
    /// `/ide` file-tree + viewer panel (Some when open).
    ide: Option<Ide>,
    /// `/memory` full-screen timeline panel (Some when open).
    memory: Option<MemPanel>,
    /// 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.state == State::Awaiting
            || self.transcript_view.is_some()
            || self.model_menu.is_some()
            || self.effort_panel.is_some()
            || self.theme_panel.is_some()
            || self.plugins_panel.is_some()
            || self.review_open
            || self.memory.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;
        }

        self.quitting = true;
        self.interrupting = true;
        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();
        self.rx = None;

        Some(cmd::cmd(move || async move {
            if let Some(abort) = host_tool_abort {
                abort.abort();
            }

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

            Msg::QuitReady
        }))
    }

    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)
    }
}

fn approval_menu_lines(label: &str, selected: usize, width: usize) -> Vec<String> {
    approval_prompt(label, selected).lines(width as u16, APPROVAL_PANEL_HEIGHT)
}

const APPROVAL_PANEL_HEIGHT: usize = 5;
const FULLSCREEN_APPROVAL_ROWS_BELOW: usize = 1;

fn approval_rows_below_for(transcript_open: bool, composer_rows_below: usize) -> usize {
    if transcript_open {
        FULLSCREEN_APPROVAL_ROWS_BELOW
    } else {
        composer_rows_below
    }
}

fn approval_prompt(label: &str, selected: usize) -> ChoicePrompt {
    ChoicePrompt::new(
        format!("⏵ Run {label}?"),
        vec![
            ChoicePromptItem::new("Allow once").shortcut('y'),
            ChoicePromptItem::new("Allow all tools this session").shortcut('a'),
            ChoicePromptItem::new("Deny").shortcut('n').danger(),
        ],
    )
    .selected(selected)
    .indent(2)
    .marker("")
    .title_color(TN_YELLOW)
    .text_color(TN_FG)
    .muted_color(TN_GRAY)
    .danger_color(TN_RED)
    .selected_colors(TN_FG, SURFACE_SELECTED)
    .hint("Enter select · ↑/↓ · 1–3 · Esc")
}

fn approval_overlay_y_offset(screen_height: usize, row_count: usize, rows_below: usize) -> u16 {
    screen_height
        .saturating_sub(rows_below)
        .saturating_sub(row_count)
        .min(u16::MAX as usize) as u16
}

#[cfg(test)]
mod tests;