mnml-rs 0.2.14

A NvChad-style terminal IDE in Rust — vim or standard editing, LSP, git, and an embedded HTTP client.
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
// The LSP client builds a deeply-nested `serde_json::json!` literal for
// `initialize` capabilities; the macro can recurse past the default 128
// frames. 256 is comfortable.
#![recursion_limit = "256"]
// `doc_lazy_continuation`: mnml's doc comments deliberately use aligned,
// non-indented continuation lines for module / keymap lists (see the crate
// docs below) — that house style is intentional, not a lint to chase.
// `type_complexity`: the UI layer carries some genuinely complex closure /
// tuple types (render callbacks, rect registries) where a `type` alias would
// hurt readability more than help.
#![allow(clippy::doc_lazy_continuation, clippy::type_complexity)]

//! mnml — a NvChad-style terminal IDE.
//!
//! Crate layout (P0 — the editor-shell skeleton; later tracks add modules):
//!   - `editor` / `edit_op` / `clipboard` — the text-editing core (operations, not keys).
//!   - `input`                            — the pluggable input layer (vim / standard keymaps).
//!   - `buffer` / `pane` / `layout` / `focus` / `app` — the open-thing + window state.
//!   - `command` / `config`               — the command registry + TOML config.
//!   - `tree` / `git`                     — the file-tree rail + git status.
//!   - `ui`                               — the (backend-agnostic) render path + theme + icons.
//!   - `tui` / `headless` / `ipc`         — the terminal event loop, the virtual-screen loop, the file-IPC channel.
//!
//! See `CLAUDE.md` for the full design.

pub mod ai;
pub mod ai_usage;
pub mod app;
// Port-back helpers from the retired `rqst` app (2026-06-19).
// `jwt`: claims-only JWT decoder; `auth`: bearer-token extraction
// from clipboard text; `cookies`: small cookie-jar helpers; `sse`:
// minimal Server-Sent Events parser (Anthropic/OpenAI streams).
pub mod auth;
pub mod cookie_jar;
pub mod cookies;
pub mod jwt;
pub mod sse;
pub mod websocket;
// `mod aws` was split out to the standalone mnml-aws-codebuild
// binary in 2026-06.
// `mod azdevops` was split out to the standalone mnml-forge-azdevops
// binary in 2026-06.
pub(crate) mod browser_pane;
pub(crate) mod buffer;
pub(crate) mod cdp;
pub(crate) mod cheatsheet;
pub(crate) mod claude_agents;
pub(crate) mod clipboard;
pub mod command;
pub(crate) mod completion;
pub mod config;
pub(crate) mod context_menu;
pub mod coverage;
pub(crate) mod dap;
pub mod data_root;
pub mod e2e;
pub(crate) mod ecs_runner;
pub(crate) mod ecs_runner_trigger;
pub mod edit_op;
pub(crate) mod editor;
pub(crate) mod editorconfig;
pub(crate) mod flash;
pub(crate) mod focus;
pub(crate) mod formatter;
pub(crate) mod fuzzy;
pub(crate) mod git;
pub mod glyph_builder;
pub mod icon_catalog;
pub mod launcher_template;
pub mod marketplace;
pub(crate) mod peek_overlay;
// `mod github` was split out to the standalone mnml-forge-github
// binary in 2026-06.
// `mod gitlab` was split out to the standalone mnml-forge-gitlab
// binary in 2026-06.
pub(crate) mod grep_pane;
pub mod headless;
pub mod highlight;
pub(crate) mod hover;
pub mod http;
pub(crate) mod image;
pub(crate) mod input;
pub(crate) mod integration_detect;
pub mod integration_manifest;
pub mod ipc;
pub(crate) mod layout;
pub(crate) mod linter;
pub(crate) mod lsp;
pub(crate) mod markdown_outline;
pub(crate) mod now_playing;
pub(crate) mod pane;
pub(crate) mod picker;
// `mod pipeline_log` was removed after the 2026-06 SCM split — no
// in-tree host populates it any more.
pub(crate) mod anthropic_api;
pub(crate) mod cloud_agent_run;
pub mod dock;
pub mod menu_bar;
pub(crate) mod mount;
pub(crate) mod mount_manifest;
pub(crate) mod new_cloud_agent_wizard;
pub(crate) mod new_cloud_run_wizard;
pub(crate) mod playwright;
pub(crate) mod prompt;
pub(crate) mod pty_pane;
pub(crate) mod regex_outline;
pub(crate) mod request_pane;
pub(crate) mod scm;
pub(crate) mod shell_prompt;
pub(crate) mod signature;
pub(crate) mod snippets;
pub(crate) mod tools;
pub(crate) mod tree;
pub mod tui;
pub mod ui;
pub mod update_check;
pub(crate) mod whichkey;

/// One clickable button in the `Pane::GitGraph` top toolbar.
/// The toolbar's `(rect, pane_id, action)` entries land on
/// `app.rects.git_toolbar_buttons`; the mouse handler matches the
/// rect + fires via `App::run_git_toolbar_action`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GitToolbarAction {
    /// `git pull --ff-only`
    Pull,
    /// `git push` (auto `--set-upstream` on first push)
    Push,
    /// `git fetch --all --prune`
    Fetch,
    /// Open the branch picker (`git.checkout`)
    BranchPicker,
    /// Open the commit-message prompt (`git.commit`)
    Commit,
    /// Open the stash-push prompt (`git.stash`)
    Stash,
    /// `git stash pop` of the most-recent stash
    StashPop,
    /// Open the reflog picker — recovery surface for "I just rebased
    /// and lost a commit" flows.
    Reflog,
    /// `git fetch --all --prune` across every configured repo, then
    /// refresh the rail's branch / worktree / PR lists.
    RefreshRepos,
    /// Cycle the active repo when the workspace has multiple
    /// `[[workspaces]]` or detected git roots. Fires the
    /// `git.next_repo` palette command.
    SwitchRepo,
    /// Toggle the per-line blame gutter (`git.blame_toggle`).
    BlameToggle,
    /// Undo the last commit — `git reset --soft HEAD~1` (keeps the
    /// changes staged; never touches the working tree). The undone
    /// commit's hash is captured so `Redo` can re-point HEAD back.
    Undo,
    /// Re-apply the most recently undone commit — `git reset --soft`
    /// to the captured hash. No-op when the undo stack is empty.
    Redo,
}

impl GitToolbarAction {
    /// Human-readable tooltip label — mouse-round-16 F6 2026-07-17.
    pub fn tooltip_label(self) -> &'static str {
        match self {
            Self::Pull => "pull (git pull --ff-only)",
            Self::Push => "push (git push, --set-upstream on first)",
            Self::Fetch => "fetch (git fetch --all --prune)",
            Self::BranchPicker => "switch branch (git.checkout)",
            Self::Commit => "commit… (git.commit)",
            Self::Stash => "stash push (git.stash)",
            Self::StashPop => "stash pop",
            Self::Reflog => "reflog (recover a lost commit)",
            Self::RefreshRepos => "refresh all repos",
            Self::SwitchRepo => "switch active repo",
            Self::BlameToggle => "toggle blame gutter",
            Self::Undo => "undo last commit (soft-reset HEAD~1)",
            Self::Redo => "redo last undone commit",
        }
    }
}

/// One clickable action inside the `Pane::GitGraph` WIP detail panel.
/// Click on a "Stage All" button ⇒ `StageAll`; click on a file row's
/// `[+]` ⇒ `StageFile(path)`. The corresponding rect lives on
/// `app.rects.wip_buttons` — the renderer pushes one entry per
/// painted button; `tui::dispatch_mouse` matches the click + fires
/// the action via `App::run_wip_action`.
#[derive(Debug, Clone)]
pub enum WipAction {
    /// `git add -A` (or equivalent) — stage every change at once.
    StageAll,
    /// `git reset` — unstage every staged file.
    UnstageAll,
    /// `git add <path>` — stage one file.
    StageFile(std::path::PathBuf),
    /// `git restore --staged <path>` — unstage one file.
    UnstageFile(std::path::PathBuf),
    /// Open the modal commit-message prompt (same as the `c` chord on
    /// the WIP row). Click the `Commit` button in the WIP detail's
    /// commit section.
    OpenCommitPrompt,
    /// Trigger AI commit-message generation (same as the `C` chord on
    /// the WIP row). Click the `AI Message` button in the WIP detail's
    /// commit section. When the WIP detail's inline textarea is
    /// available, the result fills the textarea instead of opening
    /// the modal `PromptKind::GitCommit` prompt.
    RequestAiCommitMessage,
    /// Wipe the inline commit-message textarea in the WIP detail.
    /// Click the `Clear` button next to `Commit` / `AI Message`.
    ClearCommitDraft,
}

/// One clickable action on the `Pane::Diff` top toolbar. The
/// corresponding rect lives on `app.rects.diff_toolbar_buttons`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffToolbarAction {
    /// Switch the diff to inline (unified) rendering.
    ViewInline,
    /// Switch to per-hunk collapsed rendering.
    ViewHunk,
    /// Switch to Splitumn side-by-side rendering.
    ViewSplit,
    /// Toggle line-wrap.
    ToggleWrap,
    /// Close the diff view — clears the embedded diff when shown
    /// inside a `Pane::GitGraph`, or closes the pane when standalone
    /// `Pane::Diff`. Bound to the `[×]` chip on the toolbar so the
    /// gesture is discoverable.
    Close,
}

/// Clickable chips painted on the right side of the `> GIT` rail header
/// row — one-click access to common git ops without expanding the section
/// or memorizing keyboard chords. Rects are registered on
/// `app.rects.rail_git_header_buttons`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GitRailHeaderAction {
    /// Fetch from origin.
    Fetch,
    /// `git pull --ff-only`.
    Pull,
    /// `git push` (refuses without an upstream + falls back to set-upstream).
    Push,
    /// Stage every change (`git add -A` against the active repo).
    StageAll,
    /// Open the commit prompt (existing `git.commit`).
    Commit,
    /// Open the commit graph (existing `git.graph`).
    Graph,
}

/// Which clickable chip the mouse is currently hovering over. Drives the
/// 500ms-delayed tooltip overlay shown next to the chip — see
/// `App.hover_chip` + `ui::tooltip` + `HOVER_TOOLTIP_DELAY_MS`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HoverChip {
    /// Statusline mode chip (EDIT / VIEW / TREE / INSERT / NORMAL / …).
    StatuslineMode,
    /// Statusline git-branch chip ( main +N …).
    StatuslineBranch,
    /// Statusline workspace / active-repo chip ( name).
    StatuslineWorkspace,
    /// Statusline clock chip (HH:MM or HH:MMZ).
    StatuslineClock,
    /// Stress meter — 4-block bar that fills as p95 frame time
    /// climbs. Tooltip shows the actual numbers. 2026-07-11.
    StatuslineStress,
    /// Statusline AI Claude quota chip. Click opens
    /// `Pane::ClaudeUsage` (`:ai.claude_usage`).
    StatuslineAiClaude,
    /// Statusline AI Codex activity chip. Click opens
    /// `Pane::CodexUsage` (`:ai.codex_usage`).
    StatuslineAiCodex,
    /// Mirror stress meter in the top-right cluster. Same tooltip
    /// content; kept as a separate variant so `hover_chip_at` can
    /// route each rect independently. 2026-07-12.
    PaletteStress,
    /// A specific toast box in the stack. `usize` indexes into
    /// `App.toast_stack`. Setting this hover chip pauses the toast's
    /// TTL so users can read it without it aging out. 2026-07-12.
    ToastBox(usize),
    /// A `> GIT` rail-header chip (one per action enum).
    RailHeaderChip(GitRailHeaderAction),
    /// A `Pane::GitGraph` top-toolbar chip (Undo/Redo/Pull/Push/…).
    /// mouse-round-16 F6 2026-07-17 — was: no tooltip on any of
    /// the 9 chips.
    GitToolbarChip(GitToolbarAction),
    /// A bufferline tab (carries the pane id). Tooltip shows the full path
    /// + dirty state — `display_name()` is workspace-relative + truncated.
    BufferlineTab(crate::layout::PaneId),
    /// A diff toolbar chip (Hunk / Inline / Split / Wrap / Close).
    DiffToolbar(DiffToolbarAction),
    /// A fold-collapsed chip (`⋯ N hidden`) — tooltip explains click to expand.
    FoldChip,
    /// A code-lens chip (`⚡ <title>`) — tooltip shows the full title in case
    /// the rendered chip got truncated.
    CodeLensChip,
    /// A split-pane divider — the resize handle between two panes.
    /// Tooltip explains the drag + double-click affordances.
    /// mouse-round-9 SEV-2 2026-07-11.
    SplitDivider,
    StatuslineLsp,
    StatuslineWrap,
    StatuslineAutosave,
    StatuslineFilesize,
    StatuslineLnCol,
    /// Bufferline launcher-icon — the `usize` indexes
    // 2026-08-01 (P2) — LauncherIcon variant deleted with the
    // LauncherIcon struct retirement. All chip hover-tooltip
    // routing goes through IntegrationIcon.
    /// File-tree toolbar icon row at the top of the rail. The
    /// `&'static str` is the command id (e.g. `"file.new_folder"`)
    /// stored alongside the rect in `app.rects.tree_icon_buttons`.
    TreeIcon(&'static str),
    /// 2026-06-21 — Claude Agents dashboard topbar chip. Carries
    /// the chip kind so the tooltip text can describe what each
    /// click cycles. Rect is stored in
    /// `app.rects.claude_agents_topbar_chips`.
    ClaudeAgentsTopbarChip(crate::ui::TopbarChipKind),
    /// The primary workspace header (`> WORKSPACE-NAME`) — tooltip
    /// reveals the absolute path so the user can confirm which
    /// directory mnml actually opened in.
    WorkspaceHeader,
    /// An extra workspace header from `[[workspaces]]` — the `usize`
    /// indexes `App.extra_workspaces`.
    ExtraWorkspaceHeader(usize),
    /// One icon in the rail's INTEGRATIONS section — `usize` indexes
    /// `App.config.ui.integration_icons`.
    IntegrationIcon(usize),
    /// The bufferline `+` chip that opens a new tab. Discovered via
    /// the mouse-hunt finding "bufferline + new-tab has no tooltip"
    /// (2026-06-07 chrome hunt #288).
    BufferlineNewTab,
    /// #polish 2026-07-06 r2 — hover on the `TABS N` chip at the
    /// top-right of the bufferline. Tooltip explains that click
    /// switches tab-pages / creates a new one.
    BufferlineTabsLabel,
    /// The bufferline `●━` theme-toggle pill (handle-left / handle-
    /// right depending on whether `theme_toggle` is at the primary
    /// or secondary theme).
    BufferlineThemeToggle,
    /// The `×` / `●` close badge inside a bufferline tab. Carries
    /// the same PaneId as the tab — the tooltip mentions whether a
    /// click would save (dirty) or close (clean), matching the
    /// dirty-dot-doubles-as-save semantic that landed on the right-
    /// click menu in this batch.
    BufferlineTabClose(crate::layout::PaneId),
    /// The window-level close (top-right of the bufferline strip).
    /// Closes the whole mnml process via `app.quit`.
    BufferlineWindowClose,
    /// A session-tab in the Sessions activity panel — Pty (Claude Code /
    /// Codex / shell) session. Tooltip shows a preview of the last few
    /// messages for Claude sessions; falls back to profile info for
    /// shells / Codex. (#12)
    SessionsTab(crate::layout::PaneId),
    /// Activity bar icon (left rail). Tooltip names the section.
    /// vscode-mouse-2026-06-10 SEV-3 #2.
    ActivityBarIcon(crate::app::ActivitySection),
    /// `♪ <track>` statusline now-playing chip. Tooltip names the
    /// source (mixr file / macOS Music / Spotify) + full track when
    /// the chip text is truncated.
    /// vscode-mouse-2026-06-10 SEV-3 #3.
    StatuslineNowPlaying,
    /// Palette-bar back-arrow chip (previous buffer in MRU order).
    /// vscode-mouse-2026-06-10 SEV-3 #4.
    PaletteBackButton,
    /// Palette-bar forward-arrow chip (next buffer in MRU order).
    PaletteForwardButton,
    /// Palette-bar dropdown chevron (opens the recents picker).
    PaletteDropdownButton,
    /// The H/V split-editor button at the right end of a tab strip
    /// (bufferline OR per-leaf strip). `SplitDir::Horizontal` → the
    /// side-by-side button; `SplitDir::Vertical` → the stacked button.
    SplitStripButton(crate::layout::SplitDir),
    /// The terminal-launch button at the right end of a tab strip
    /// (immediately left of the H/V buttons). Click opens a new
    /// shell in a split.
    SplitStripTermButton,
    /// AI launcher button at the right end of a per-leaf tab
    /// strip — opens Claude / Codex in a split.
    SplitStripAiButton,
    /// Palette-bar sidebar toggle (codicon layout-sidebar-left/off)
    /// — click fires view.toggle_tree (Ctrl+B).
    PaletteSidebarButton,
    /// Palette-bar right-panel toggle (mirror of sidebar). Click
    /// fires view.toggle_right_panel.
    PaletteRightPanelButton,
    /// A tab chip on the right-panel tab strip — carries the
    /// pane id of the hosted tab so the tooltip can show the
    /// full label and the pane's tab_title (e.g. file path /
    /// problem counts). v3 right-panel polish.
    RightPanelTab(crate::layout::PaneId),
    /// The `×` close button on the right-panel tab strip. Closes
    /// the active tab on click; tooltip explains which tab.
    RightPanelClose,
    /// Palette-bar search chip — the workspace name + magnifier.
    /// Click fires the command palette.
    PaletteSearchChip,
    /// Palette-bar `+` chip — opens integrations.add discovery.
    PaletteAddIntegration,
    /// Per-leaf split tab strip tab chip (`(rect, leaf_active,
    /// tab_pane)`). Stores the tab pane id for tooltip lookup
    /// (file path, dirty state, etc.).
    SplitTabChip(crate::layout::PaneId),
    /// Per-leaf split tab strip close badge.
    SplitTabClose(crate::layout::PaneId),
    /// Per-leaf split tab strip `+` chip (stores the leaf's active
    /// pane id — same key `split_tab_plus_buttons` uses).
    SplitTabPlus(crate::layout::PaneId),
    /// Agents-panel header chip — type encodes which one (New
    /// session, from PR, or view toggle).
    AgentsPanelChip(AgentsPanelChipKind),
    /// Cloud Agents `+ New Cloud Run` button.
    CloudAgentsNewRunButton,
    /// Cloud Agent Run detail pane: auto-refresh interval cycler.
    CloudRunAutoRefresh,
    /// Cloud Agent Run detail pane: manual refresh chip.
    CloudRunRefresh,
    /// Activity-bar gear icon — opens settings menu.
    ActivityBarGear,
    /// Dock kebab (⋮) menu trigger.
    DockKebab,
    /// Dock empty-state `+ dock` chip.
    DockEmptyChip,
    /// Statusline play / pause chip (mixr controls).
    StatuslineMixrPlay,
    /// Statusline fast-forward chip (mixr controls).
    StatuslineMixrFfwd,
    /// Statusline test-runner chip — click focuses test output.
    StatuslineTestChip,
    /// qa-feature 2026-06-30 — a specific cell in the GitGraph
    /// pane's lane column. `pane_id` locates the pane;
    /// `commit_idx` is the index into the pane's `commits` vec
    /// (excluding the WIP virtual row); `lane_idx` is the column
    /// within the graph. Tooltip walks newer commits in the same
    /// lane to find the closest branch ref and displays it.
    GitGraphLane {
        pane_id: usize,
        commit_idx: usize,
        lane_idx: usize,
    },
    /// qa-feature 2026-07-01 — hover on a commit's subject cell
    /// in the GitGraph pane. Tooltip shows the full commit
    /// subject (unclipped) + author name. Useful when the pane
    /// is narrow enough that the subject truncates with `…`.
    GitGraphCommitMsg {
        pane_id: usize,
        commit_idx: usize,
    },
    /// #21 v5 — hover on one of the Request pane's top-bar
    /// chips (Method / Env / Send / Save / Clear / Code).
    /// Tooltip explains what the click does + notes right-
    /// click for the kebab menu.
    RequestTopBarChip(RequestTopBarChip),
    /// #21 v7 — hover on the `[▥ ▤]` split-orientation toggle
    /// chip on a Request pane. Tooltip names the current
    /// orientation + the alternative.
    RequestSplitToggle,
    /// The `[⇔]` chip that opens a side-by-side edit split
    /// (Body|Vars etc.). Tooltip explains what click does.
    RequestEditSplitChip,
    /// Hover on one of the section-toolbar chips (filter / refresh /
    /// capture / clear) on the HTTP activity-bar panel — RECENT /
    /// CAPTURED / MOCKS / COLLECTIONS / CHAINS all render some
    /// subset of these. `usize` indexes `PaneRects.http_panel_section_chips`.
    /// 2026-07-07.
    HttpSectionChip(usize),
    /// Hover on the `..` up-navigation row above the file tree.
    /// Tooltip names the parent path so the user sees where the click
    /// will land. 2026-07-07 (design-critic #3).
    TreeUpRow,
    /// Hover on one of the top-level HTTP panel toolbar chips (↺ / ↕).
    /// Command id is stashed alongside the rect on
    /// `http_panel_icon_buttons`; the tooltip callback resolves the
    /// title from the command registry.
    HttpToolbarChip(usize),
    /// Hover on the 1-cell divider between the primary + secondary
    /// sides of a Request-pane edit split. Behavior is different from
    /// the tree/right-panel edge grips (click cycles preset ratios
    /// instead of drag-resize) so the tooltip warns the user.
    RequestEditSplitDivider,
    /// Hover on the `+` chip on a COLLECTIONS row (adds a new
    /// request to that collection). `usize` indexes
    /// `PaneRects.http_panel_collection_new_request_chips`.
    /// 2026-07-07 (vscode-mouse SEV-2 #2).
    HttpCollectionAddRequestChip(usize),
    /// Hover over a `{{VAR}}` token in a Request pane's URL / body /
    /// value cell. Tooltip shows the resolved value (or "undefined"
    /// when the env doesn't have this key), plus a hint that clicking
    /// jumps to the env-file definition. `String` is the var name.
    /// Index is stored on `App.hover_chip_var_idx` (not the name)
    /// because HoverChip is Copy-only — the tooltip callback looks
    /// the name back up from `PaneRects.request_var_click_rects`.
    RequestVarToken(usize),
    /// #21 v8 — hover on the response bar's `copy` chip.
    RequestResponseCopy,
    /// #21 v8 — hover on the response bar's `wrap` chip.
    RequestResponseWrap,
    /// 2026-07-09 — hover on the response bar's `⚡ AI` chip
    /// (shown only when the response looks like a failure).
    RequestResponseAiPrompt,
    /// #21 v8 — hover on the response bar's `{ } Format` chip.
    RequestResponseFormat,
    /// #21 v9 — hover on the pending-undo chip. Tooltip explains
    /// the keyboard shortcut + shows what will be undone.
    PendingUndoChip,
    /// #21 v10 — hover on the inline `+` new-request chip on the
    /// bufferline (visible when at least one Request pane is
    /// open). Tooltip distinguishes it from the far-right `+`
    /// new-tab-page button.
    BufferlineNewRequest,
    /// #polish 2026-07-06 — hover on any scrollbar. Tooltip
    /// explains the click / drag behavior once (users often
    /// don't know the whole track is clickable).
    ScrollbarThumb,
    /// #polish 2026-07-06 — hover on the right-panel resize
    /// grip. Explains drag-to-resize + double-click-to-reset.
    RightPanelGrip,
    /// #polish 2026-07-06 — hover on the tree rail resize
    /// grip. Same treatment.
    TreeRailGrip,
    /// #polish 2026-07-06 — hover on a menu bar word (mnml /
    /// File / Edit / Selection / …). Tooltip explains the
    /// click behavior + Alt+<letter> accelerator.
    MenuBarWord(usize),
    /// Task #929 (2026-08-12) — hover on an *open dropdown item*
    /// inside a menu-bar menu (e.g. `File → New file`). Routes
    /// through `InfoViewTarget::MenuItem { menu, item }` so the
    /// curated `menu_item_copy` entries in
    /// `src/ui/info_view_copy.rs` reach the info-panel — until
    /// this variant landed, hovered items only surfaced the
    /// generic `MenuBarWord` (parent) copy.
    /// `menu_idx` indexes `menu_bar::bar(app)`; `item_idx` uses
    /// the *encoded* shape produced by `ui/menu_bar.rs`:
    ///   - top-level row: raw `i` (< 1000),
    ///   - submenu row:   `1000 + parent_item_idx*100 + sub_i`.
    /// `resolve_menu_bar_item_copy` in `ui/info_view_copy.rs`
    /// decodes this on the read side — never hand-decode from
    /// this comment alone; grep for the producer to stay in sync.
    MenuBarItem {
        menu_idx: usize,
        item_idx: usize,
    },
    /// #polish 2026-07-06 — file-name chip on the statusline
    /// (glyph + display_name + dirty marker). Tooltip shows
    /// full absolute path + dirty state; click reveals in tree.
    StatuslineFile,
    /// #polish 2026-07-06 — diagnostics summary chip (spans
    /// err + warn segs). Tooltip breaks down counts.
    StatuslineDiagnostics,
    /// #polish 2026-07-06 — language / filetype chip.
    /// Tooltip names the ext; click opens language picker.
    StatuslineLanguage,
    /// #polish 2026-07-06 — enclosing-symbol crumb chip.
    /// Tooltip shows the untruncated symbol name.
    StatuslineSymbol,
    /// #polish 2026-07-06 — active-branch PR badge (`BB#42`).
    /// Tooltip shows PR title; click opens the PR in browser.
    StatuslinePr,
    /// #polish 2026-07-06 — macro-recording chip (`● rec @<reg>`).
    /// Tooltip names the register; click stops recording.
    StatuslineMacroRec,
    /// #polish 2026-07-06 — active-find chip (` /q N/M `).
    /// Tooltip shows the full query; click reopens find prompt.
    StatuslineFind,
    /// #polish 2026-07-06 — selection-size chip (` Sel N `).
    /// Tooltip only.
    StatuslineSel,
    /// #polish 2026-07-06 — LSP `$/progress` chip. Tooltip
    /// shows the untruncated title.
    StatuslineProgress,
    /// #polish 2026-07-06 — background-tasks spinner chip.
    /// Tooltip lists what's running.
    StatuslineBgTasks,
    /// #polish 2026-07-06 — inline-suggestion in-flight chip.
    /// Tooltip only.
    StatuslineAi,
    /// #polish 2026-07-06 — hover on a mark rendered in the
    /// gutter's sign column (git change / diagnostic dot /
    /// breakpoint / DAP arrow). `line_no` is the file-line the
    /// mark represents; `kind` drives the tooltip label.
    GutterMark {
        pane_id: crate::layout::PaneId,
        line_no: usize,
        kind: GutterMarkKind,
    },
    /// Task #875 (R5 SEV-3 F6) — a numbered bufferline tab-page pip
    /// (`1`/`2`/…) at the right edge of the bufferline chrome row.
    /// Click switches tab pages (mnml's Editor Groups equivalent).
    /// Carries the 0-based page index so the tooltip can name the
    /// page's active pane.
    BufferlineTabPage(usize),
    /// Task #875 — the tiny `×` close-badge inside a tab-page pip.
    /// Only appears for the non-active page. Click closes the page.
    BufferlineTabPageClose(usize),
    /// Task #875 (R5 SEV-3 F7) — Integrations panel tab-strip chips
    /// (Installed / Marketplace filter tabs, `⟳` refresh, `A-Z ▾`
    /// sort). Distinct variants so tooltips can name their specific
    /// action.
    IntegrationsTabInstalled,
    IntegrationsTabMarketplace,
    IntegrationsTabRefresh,
    IntegrationsTabSort,
    /// Task #875 (R5 SEV-3 F8) — statusline test-coverage chip
    /// (`53% ±0.0` etc.). Click opens the coverage overlay;
    /// right-click is the coverage context menu.
    StatuslineCoverage,
    /// A dynamic statusline chip declared by an integration
    /// manifest's `[[statusline_segments]]` block (or by an IPC
    /// `statusline_set_segment` call from a running integration). The
    /// `usize` indexes `PaneRects::statusline_segment_hits` — a
    /// per-frame vec of `(Rect, segment_id)` populated by
    /// `ui::statusline::draw`. Click routes to the segment's
    /// `click_command`; tooltip / info-panel copy pulls from the
    /// declaring manifest's `[[statusline_segments]]` entry.
    /// 2026-08-17 (data-driven statusline chips).
    StatuslineSegment(usize),
}

/// What was painted in the gutter sign column for a given line —
/// drives the tooltip's copy. Ordering matches the sign-column
/// priority in `editor_view` (DapArrow wins, then breakpoints,
/// then diagnostics, then git marks).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GutterMarkKind {
    /// `▶` — DAP is stopped on this line.
    DapArrow,
    /// `◆` — breakpoint with a condition.
    ConditionalBreakpoint,
    /// `●` (red) — plain breakpoint.
    Breakpoint,
    /// `●` colored by severity (red / yellow / cyan / grey).
    Diagnostic(crate::lsp::Severity),
    /// `▎` (green / blue / red) — git added / modified / removed.
    GitChange(crate::git::diff::SignKind),
}

/// Which top-bar chip on the Request pane was hovered. Kept
/// isolated from `HoverChip` for easier future extension.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestTopBarChip {
    Method,
    Env,
    Send,
    Save,
    Clear,
    Code,
}

/// Which Agents-panel header chip a `HoverChip::AgentsPanelChip`
/// references. Used by `tooltip::describe` to render the right
/// label without expanding the parent enum.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentsPanelChipKind {
    /// `+ New session` — opens a single Claude Code session.
    NewSession,
    /// `+ from PR` — opens the multi-PR wizard.
    FromPr,
    /// View toggle (workspace / status grouping).
    ViewToggle,
}

/// One row in the F1 click-discovery overlay. Each variant maps to a list
/// of on-screen rects that the renderer flashes when the user clicks the
/// row in the panel. See `ui::discovery` + `App::discovery_flash`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiscoveryCategory {
    StatuslineMode,
    StatuslineBranch,
    StatuslineWorkspace,
    StatuslineClock,
    BufferlineTabs,
    RailGitHeader,
    EditorGutter,
    DiffToolbar,
    FoldChips,
    CodeLensChips,
    SplitDividers,
}

/// One clickable per-hunk action chip in the Hunk view's header
/// row. The corresponding rect lives on
/// `app.rects.diff_hunk_buttons`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffHunkAction {
    /// Apply this hunk to the index (`git apply --cached`).
    Stage,
    /// Reverse-apply this hunk against the index (`git apply
    /// --cached --reverse`).
    Unstage,
    /// Reverse-apply this hunk against the working tree —
    /// destructive, prompts for confirmation in the dispatcher.
    Discard,
}

/// Crate-wide test lock protecting mutations of process env vars
/// that multiple test modules touch (`HOME`, `XDG_CONFIG_HOME`,
/// etc.). Cargo runs tests in parallel across modules; each module
/// previously had its own local `home_lock()` static, so a discovery
/// test could clobber a cdp test's `HOME` mid-run. Route every
/// `set_var("HOME", …)` in tests through `crate::test_env_lock()`
/// so they serialize across the whole test binary.
///
/// Ubuntu-latest CI (higher default `--test-threads`) exposed the
/// race on 2026-08-03; macOS locally never lost.
#[cfg(test)]
pub fn test_env_lock() -> &'static std::sync::Mutex<()> {
    static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
    LOCK.get_or_init(|| std::sync::Mutex::new(()))
}

/// RAII guard for a single env var. Grabs the previous value on
/// `set()`, restores it on drop — including drop-during-unwind, so
/// a failing `assert!` mid-test still runs the restore. Pair with
/// [`test_env_lock()`] so cross-module writes stay serialized:
///
/// ```ignore
/// let _lk = crate::test_env_lock().lock().unwrap_or_else(|e| e.into_inner());
/// let _home = crate::EnvGuard::set("HOME", tmp.path());
/// // …test body — panic-safe. HOME restores when _home drops.
/// ```
///
/// Motivation: pre-2026-08-03 test bodies restored env vars with a
/// manual `if let Some(prev) = …` at the bottom of the fn. An
/// assertion failure earlier in the body skipped the restore, so
/// the tempdir HOME leaked into every subsequent test on that
/// binary run — exactly the flake pattern the shared lock was
/// meant to close. Drop-based restore is the only shape that
/// survives panics.
#[cfg(test)]
pub struct EnvGuard {
    key: &'static str,
    prev: Option<std::ffi::OsString>,
}

#[cfg(test)]
impl EnvGuard {
    /// Set `key` to `value`, remembering the previous value so it
    /// can be restored on drop. `key` is `&'static str` so a stray
    /// String doesn't accidentally end up as the env var name.
    pub fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {
        let prev = std::env::var_os(key);
        // SAFETY: env var write. Callers must hold `test_env_lock()`
        // for cross-module ordering. `EnvGuard::set` itself is not
        // safe against concurrent writers to the same key — the lock
        // provides that.
        unsafe { std::env::set_var(key, value.as_ref()) };
        Self { key, prev }
    }

    /// Remove `key` for the duration of this guard. Prior value
    /// is restored on drop (or `None` → var stays removed).
    pub fn remove(key: &'static str) -> Self {
        let prev = std::env::var_os(key);
        // SAFETY: same as `set`.
        unsafe { std::env::remove_var(key) };
        Self { key, prev }
    }
}

#[cfg(test)]
impl Drop for EnvGuard {
    fn drop(&mut self) {
        // SAFETY: env var write during Drop. Runs during normal
        // return AND during panic unwinding, so a failed assertion
        // still restores.
        unsafe {
            match self.prev.take() {
                Some(v) => std::env::set_var(self.key, v),
                None => std::env::remove_var(self.key),
            }
        }
    }
}