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
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
# mnml TODO

Living list of work that's been considered but deliberately deferred.
Not a wishlist — only items where the scope/shape is already understood
and the only thing missing is a session to do it in.

## HTTP

### WebSocket support
**Status: complete.** Both tracks shipped:

- **One-shot**: `:ws.send` fires a single frame against a `.ws`
  JSON file (`{ url, message, timeout_ms?, headers? }`) and lands
  the reply in `[ws-response]`.
- **Persistent**: `:ws.connect` opens a `Pane::Websocket` with a
  live `tungstenite` worker, `[ws-<host>]` scratch log (`← text`
  in / `→ text` out), and `:ws.send_message` / `:ws.disconnect`
  companion commands. Multi-connection works: each `:ws.connect`
  spawns a fresh pane. `:ws.history` picker replays past
  connections from `~/.mnml/ws-history/<slug>/history.jsonl`.
- **Runtime knobs** (2026-07-03): `[ws]` config table —
  `subprotocols` (Sec-WebSocket-Protocol header), `ping_interval_secs`
  (default 30, 0=off), `reconnect_max_attempts` (default 3, 0=off,
  1s→2s→4s→8s→16s backoff).

Not planned: cookie-jar reuse on the upgrade handshake (mnml's
cookie jar is HTTP-only for now). Open a fresh ticket if there's
a specific workflow that needs it.

## Integrations

### GitHub Issues sibling (mnml-tracker-github)
**Shape:** a `mnml-tracker-*` sibling that mirrors the existing
`mnml-tracker-jira` + `mnml-tracker-linear` tools but for GitHub
Issues. Uses `gh api` under the hood (no new deps — the auth chain
that already backs `mnml-forge-github` reuses cleanly). Manifest
registers as `github_issues` (distinct from `github` forge chip).

Tabs: mirror the jira / linear shape — `Assigned to me`,
`Mentioned`, `Created by me`, plus configurable saved-search tabs
(e.g. by label / milestone / repo). Row Enter opens the console
URL; Enter on a body cell opens a scratch pane with the full
markdown for quick reference.

Sibling repo goes at `mnml-tracker-github` alongside the jira +
linear ones. Integration manifest registers `<leader>ig` chord for
"GitHub Issues: open". First-party family entry, not community.

## Search / Find

### Unified Telescope-style picker framework
**Status:** design 2026-08-20 — highest-leverage of the search
beef-up thread. Today we have several disjoint pickers that don't
share code or UI: Ctrl+P (files, `PickerKind::File`), Ctrl+Shift+P
(commands, `PickerKind::Command`), Find activity-bar section
(grep, `App::search_*`), plus ~15 contextual pickers
(`PickerKind::*` in `src/picker.rs`). Each has its own render,
its own filter behavior, its own keybinds. NvChad users expect
Telescope: one overlay, live preview pane on the right, fuzzy
match, and cross-pivot (files → symbols → hits → def) with a
single API surface.

**Visual parity is a hard requirement.** The overlay should
LOOK like Telescope so telescope users feel at home the moment
they open it — same layout (filter+list on the left, preview on
the right), same border/color idiom, same status footer (result
count + source name + shortcut hints), same prompt-prefix
chevron. Recognition matters more than novelty here.

Shape:
  - New `src/picker/framework.rs` — a `PickerSource` trait:
    `fn query(&mut self, q: &str) -> Vec<PickerRow>` +
    `fn preview(&self, row: &PickerRow) -> PreviewContent`
    (rendered file excerpt, chunk of grep hit context, command
    help text, …).
  - Single overlay renderer: left = filter input + result list,
    right = preview pane (togglable, ratio persisted per source).
  - Migrate the 15+ `PickerKind` variants one at a time to the
    new source trait; keep the old path working during migration.
  - Cross-pivot chord: from any picker, `Tab` cycles the source
    (files → commands → grep → symbols → …) without losing the
    current query. This is the Telescope killer feature.
  - Fuzzy matcher: `nucleo` or `fuzzy-matcher` (already used?
    check). Rank by match quality + recency + pinned.

Not planned v1: multi-select in every picker (only where it
already exists — file picker for open-many); live filter debounce
(matter-of-taste; start with fire-on-Enter for grep-shape, live
for list-shape).

### Find section upgrades
**Status:** related to unified-picker but stands alone. Current
Find (activity-bar section) is fire-on-Enter grep with a single
input row and a hit list. Missing what a Telescope / VS Code
user expects:
  - Live preview pane (rendered before the framework lands — the
    Find section is the highest-value pilot).
  - Filters: case-sensitive toggle, whole-word, regex, path glob
    include/exclude (`--iglob`), file-type filter (`--type rs`).
    Chip row above the input like VS Code's search sidebar.
  - Search history dropdown (last 20 queries, up-arrow to cycle).
  - "Search in selection" (visual-mode) — restrict grep to the
    currently-selected buffer range.
  - Replace-across-hits with preview: type a replacement, see
    every hit's before/after inline, Space to toggle per-hit,
    Ctrl+Enter to commit all.
  - Multi-workspace grep already works; surface the workspace
    each hit came from in the row label.

### Palette search polish
**Status:** works well today; small quality wins outstanding.
  - Recent/favorites weighting: bump commands used in the last
    N days to the top when the query is empty; persist a
    per-workspace usage counter in `.mnml/palette-usage.json`.
  - Pane-scoped suggestions: when the current pane is a `Pty` /
    `IntegrationDetail` / `HTTP request` / …, boost commands
    relevant to that pane kind (`term.*`, `integrations.*`,
    `http.*`) above generic ones for empty-query state.
  - Keybinding hint alignment: right-align the chord glyphs so
    the eye can scan them as a column instead of chasing them
    across variable-width names.
  - Fuzzy match highlighting: bold the matched characters in
    the row label so the user sees WHY a row matched.

### AST-aware search (tree-sitter queries)
**Status:** niche but powerful. Ripgrep + regex handles 90% of
real searches; the remaining 10% is structural — "find all
function definitions matching X", "find call sites of `foo()`",
"find `impl` blocks for trait Y", "find TODO comments" (already
partly covered by the TODOs panel).

Shape:
  - `src/search/ast.rs` — thin wrapper over the tree-sitter
    query APIs already used by `highlight.rs` + `hover.rs`.
    One query per (language, intent) pair, stored under
    `assets/queries/<lang>/<intent>.scm` (Neovim / Helix's
    convention).
  - Palette command `search.ast_symbol` → picker of the
    workspace's symbols (functions, structs, impls, traits) by
    fuzzy name.
  - Palette command `search.ast_calls_of` → prompt for symbol
    name, list all call sites across the workspace.
  - Not planned v1: full LSP-based rename or refactor —
    tree-sitter queries are best-effort text-shape matches,
    not semantic. Rename stays LSP's job.

Deferred behind the framework + Find upgrades — those two are
per-user-benefit universal; AST helps a subset.

## Other (uncategorized)

### Cloud agents list: compact vs standard view modes
**Status:** user request 2026-06-27 — current row UX feels guessy.

Today each cloud-agent row is one line: `▢ TE-NNNNN  state`.
With 14+ rows in a busy workspace, you can't tell what's what
without clicking through. User wants two view modes:

1. **Compact** (current) — one line per row, scannable list
   when there are many rows.
2. **Standard / expanded** — multi-line per row showing:
   ticket title (truncated), start time / duration, last log
   excerpt, current step / heartbeat. Enough to know what the
   run is doing without drilling in.
3. **Hover tooltip** (bonus) — even more detail when the user
   mouses over: full title, all metadata, retry count,
   parameters passed to the cloud run.

Persist the choice per-workspace in
`.mnml/cloud_agents_view.toml` so each project remembers.

Shape:
  - Toggle keybind on the cloud-agents panel (default `v`?).
  - Settings overlay row.
  - `src/ui/cloud_agents_panel.rs` already has the per-row
    rendering; add a `CloudAgentsView { Compact, Standard }`
    enum to App state + branch render accordingly.

### Cloud agents / Pty: tab strip should be per-split, not global
**Status:** caught 2026-06-27 — user reported "4 tabs" when
tailing two cloud runs.

Today: opening two Tail-logs flows creates two splits AND each
split's tab strip shows BOTH viewer buffers. Visually that's
"4 tabs" even though there are only 2 buffers in 2 splits.
The user's expectation is each split's tab strip shows only
the buffers IN that split.

Two paths:
  1. **Tab strip per-split scope** — render only the buffers
     attached to this split's tab group. Other buffers exist
     in other splits and aren't listed here. (Cleanest.)
  2. **Reuse existing split for similar opens** — clicking
     Tail logs a second time, when an existing cloudwatch
     viewer pane is open, route to that pane's split + open
     the new viewer as a TAB (not a new split). Both viewers
     visible in one split's tab strip; no second split.

Either path eliminates the "4 tabs" confusion. Path 2 is
more aggressive (changes pane-open routing); path 1 is just
UI scoping. Probably do both but path 1 first.

### + New Cloud Agent wizard — Jira source + Kepler-inspired UX
**Status:** Phase 2a shipped 2026-06-27 (`698bff3`) — GitHub PRs +
Bitbucket PRs + Manual prompt sources, Claude Agent SDK as the
agent, multi-select checkboxes, action templates. This entry
captures what's next.

**Jira "assigned to me" source** (user request 2026-06-27):
  - Auth: ATL_JIRA_TOKEN or ATL_JIRA_API_KEY env var + the
    user's already-set TATTLE_USER_JIRA_NAME for queries.
  - REST: `GET /rest/api/3/search?jql=assignee=currentUser() AND status not in (Done,Closed)`
  - Same multi-select + action template flow as PRs. Submit:
    spawn Claude with the ticket key in the prompt + git
    branch creation hint.

**Kepler-inspired enhancements (worth borrowing):**
  - **Task-centric model.** A "task" = work spanning multiple
    repos, originated from a PR OR a ticket. Sessions list
    shows tasks, not raw runs. Maps onto our cloud-agent rows
    nicely.
  - **Kanban-by-status.** Group rail rows: Needs Attention ·
    Active · Idle · Errored · Inactive (we already do partial:
    Action needed / Running / Done). Extend to match Kepler's
    five-bucket model.
  - **Agent-agnostic step.** Add an optional "Agent" step at
    the END of the wizard (defaulting to Claude). User can
    pick Codex / Open Code / future agents per-session.
    Different agents = different spawn commands but same
    submit shape.
  - **Bidirectional Console / redirect mid-session.** The
    CloudAgentRun pane could grow a small "send a message to
    the agent" textarea at the bottom (writes to the agent's
    stdin or session via SDK).

**Codex agent kind** (deferred, not urgent):
  - Spawn shape similar to Claude: `codex --print "<prompt>"`.
  - Auth via env (OPENAI_API_KEY or similar).
  - Same Action templates apply.

### + New Cloud Agent wizard — expanded scope (phase 2)
**Status:** v1 wizard scaffolding landed 2026-06-27 with the ECS runner + Claude managed paths. User then expanded the scope —
captured here for phase 2.

**New step structure (replaces the simple 2-agent picker):**

1. **Agent**: Claude Code · Codex · ECS runner · GitKraken
   Kepler · Keck (user clarification needed on the last two —
   are these distinct tools, or one product I'm missing?) ·
   Other (free-form binary).
2. **Source / trigger**: Assigned to me · Pick from PR list ·
   Pick from ticket list · Manual prompt.
3. **Multi-select** (when source is a list): checkbox list of
   PRs or tickets; the wizard fires one run per checked item OR
   one run per batch depending on the action.
4. **Action**: Triage · Review · Test · (custom suggestions —
   "Document", "Write tests", "Find regressions", "Update
   dependencies", etc).
5. **(Claude managed only) Sandbox**: Cloud · Self-hosted local
   · Self-hosted remote (Vercel/Cloudflare/Modal/etc).
6. **Review & submit.**

**Required new wiring:**
  - GitHub PR list per repo (use `gh pr list` or GraphQL).
  - Jira "assigned to me" list (REST API: `/rest/api/3/search?jql=assignee=currentUser()`).
  - Cloud-run ticket list (DynamoDB scan, wired in `ecs_runner.rs`).
  - Multi-select checkbox widget — generic, useful elsewhere.
  - Action templates per (agent, action) combo — these become
    the initial prompt sent to the agent.
  - Agent invocation per kind:
    - Claude Code: spawn `claude --resume`-style or
      `claude --new-session <prompt>` Pty pane.
    - Codex: `codex --new-session <prompt>` Pty.
    - ECS runner: existing `fire_cloud_run`.
    - GitKraken Kepler: need to investigate API.
  - For multi-select + batch: a "run N agents in parallel"
    fan-out flow with progress tracking.

### External tool install — htop, iftop, btop, …
**Status:** user request 2026-06-27 — `:tools.htop` currently
errors with "unknown command".

Today the install flow only handles `mnml-*` family siblings via
cargo. Want to extend to common terminal tools (htop, iftop, btop,
ncdu, lazygit, …) that ship via brew / apt. Same y/n prompt + Pty
install pane shape.

Shape:
  - New `src/external_tools.rs` with an EXTERNAL_TOOLS catalog —
    each entry: `id, binary, description, brew_name, apt_name,
    icon`.
  - `:tools.<id>` palette command (or `:install <id>`) routes
    through `install_external_tool` instead of `install_sibling`.
  - Install Pty runs `brew install <name>` on macOS, falls back to
    apt/dnf on Linux. Post-install auto-retry just spawns
    `:term <binary>`.
  - Surface in the Integrations Marketplace tab alongside family
    siblings — same picker UX.

Suggested initial catalog:
  - htop, btop, ncdu, lazygit, fzf, ripgrep, jq, yq, gh,
    tldr, bat, dust, watch, iftop, bandwhich

Why deferred: needs decisions on (a) catalog scope (where do we
draw the line between "useful tools" and "anything brew has"),
(b) Linux package-manager detection, (c) how the install flow
differs from cargo (brew is idempotent, apt needs sudo). Probably
needs a design doc + small PR per layer.

### Git-graph: repo dropdown + tighter sidebars
**Status:** captured 2026-06-27 — user request after looking at the
git-graph in `tattle-claude-workspace`.

Two complaints rolled into one entry:

1. **Repo selection dropdown at the top of the file browser
   panel** (left rail of the git-graph view). Currently shows the
   workspace name as plain text ("tattle-claude-workspace") right
   above the branch tree. In a multi-repo workspace
   (`[[workspaces]]` config + sub-repos discovered by
   `git::repos::discover_repos`), the user has to switch active
   repo via `:git.switch_repo` or the picker; not discoverable.
   Make the name itself the affordance — click → dropdown of
   discovered repos, select → switches the entire git-graph view
   (branches, commits, working-tree changes panel) to that repo.
   This is what GitHub Desktop / GitKraken / Tower all do.

2. **Tighten the left + right sidebars on git-graph**. The user's
   read on the proportions: left rail (LOCAL/REMOTE/WORKTREES
   tree) + right rail (WIP / Unstaged Files / Staged / Commit) are
   each ~25% of the viewport, center commit-list is ~50%. Pull
   both sidebars in by ~50-100px so the center commit message
   column gets more breathing room. The narrow center is the bit
   that's hardest to read — sidebars can spare the pixels.

Implementation sketch:
  - `src/ui/git_graph_view.rs` already lays out the three columns
    via ratatui constraints. Bump the centre weight (or set
    explicit min widths on the side columns instead of
    percentage-based).
  - For the dropdown: extend `App::repos` (already populated by
    `discover_repos`) and add a click-rect on the workspace-name
    row → opens a picker over the repos + the existing
    `switch_active_repo` accept handler.

### Multi-pane siblings (Mount or Pty)
**Status:** deferred 2026-06-26 — captured before it gets baked in.

Today: each Mount manifest = one Mount pane (one UDS, one render
loop). Each Pty profile = one Pty pane. A sibling that wants
multiple visual surfaces (main view + detail panel, dashboard +
log tail, …) has to either render both inside one pane (manual
internal split) or use the `OpenPty` tier-2 IPC to spawn a second
Pty as a side effect.

That's fine for today's single-pane siblings but won't scale to
richer integrations. Bake the multi-pane assumption into the
protocol now so we don't have to retrofit.

Shape (draft — discuss before coding):
  - Manifest gains `[[panes]]` array — each entry has its own
    `id`, `name`, `icon`, `color`, and an optional `mode`
    selector that mnml passes to the sibling on spawn
    (`--mode list`, `--mode detail`, etc.). Each entry registers
    its own activity-bar icon.
  - Clicking icon N spawns the sibling with `--mode <n.mode>`    or, if it's already running and the sibling registered itself
    for multiplexing, sends an "open pane" message over the
    existing UDS.
  - On the Pty side: a sibling can send a tier-2
    `OpenAdditionalPty { label, cmd, args, alongside? }` IPC to
    open companion panes that mnml tracks as related (e.g. closes
    them together if the user closes the parent).

Open questions: how does mnml know two panes are "the same
sibling" for status / cleanup purposes? Is it parent-PID, manifest
id, or a sibling-asserted group token? Keep cleanup safe under
crashes (sibling main pane dies, side panel becomes orphan).

### Pre-built sibling binaries (no bundling)
**Status:** deferred 2026-06-26. User explicitly chose NOT to
bundle siblings into mnml core ("the name is minimal — if the
ecosystem grows, mnml shouldn't gatekeep which integrations are
core enough to ship"). Compile time on first install (~30-60s)
remains the main UX cost; auto-retry (commit 9460403) covers the
"forgot to re-click" half of the pain.

Next step when ready: GitHub Releases with pre-built signed
binaries per sibling, served via `cargo-binstall` (or a mnml
built-in downloader). Reuse the same standard-tier runner set
mnml itself uses (audit 2026-06-26 — all five are free on public
repos):
  - macos-14 (Apple Silicon) — `aarch64-apple-darwin`
  - macos-15-intel — `x86_64-apple-darwin`
  - windows-2022 — `x86_64-pc-windows-msvc`
  - ubuntu-22.04-arm — `aarch64-unknown-linux-gnu`
  - ubuntu-22.04 — `x86_64-unknown-linux-gnu`

DON'T switch to `*-xlarge` or `*-large` runners — those are
paid even on public repos.

(2026-08-07 — `scripts/notarize-dmg.sh` was removed with the DMG
packaging drop. Siblings that want notarization would need to
re-add cert plumbing; not doing this by default since siblings
distribute via `cargo install` or their own scripts.)

Trigger: push-to-main publishes to a `main-latest` GitHub
release. `cargo-binstall` picks up the latest binary; no manual
tagging required, no forgotten-fix risk.

### Audit + re-tag siblings post-tmnl-protocol removal
**Status:** caught 2026-06-26 when a user `:install`-ed
`mnml-aws-cloudwatch-logs` and the build broke on a missing
`tmnl-protocol` workspace member. Pinned `cloudwatch_logs` to
`"main"` as a stopgap (see family_catalog.rs).

Background: 2026-06-22 we ripped tmnl-protocol out of every
sibling repo (mnml became terminal-agnostic — see commit
ce99b59 / memory). Most sibling repos still have tagged
releases (v0.1.0, v0.2.0, …) that predate the removal and
reference `tmnl-protocol` as a path/workspace dep. `cargo
install` on those tags fails immediately.

Required work (per sibling, ~30 to audit):
  1. In each `mnml-*` repo's main branch, verify the workspace
     Cargo.toml no longer lists `tmnl-protocol`
  2. If it still does, remove it + bump the version
  3. Tag a new release (v0.1.1 / v0.2.1 / etc.)
  4. Bump that sibling's `pinned_version` in
     family_catalog.rs back from `"main"` to the new tag

Easier in parallel — agent-able. Each sibling repo is small;
the fix per repo is mechanical.

Why deferred: cross-repo coordination + ~30 small PRs. The
stopgap (pin → main) works for users who hit it one-off.

### In-app icon designer for siblings + integrations
**Status:** deferred 2026-06-26 — user asked for it.

Today: each `family_catalog::FamilySibling` (35+ entries) carries
a hand-picked Nerd Font glyph as its activity-bar / rail icon.
Picking generic Nerd Font glyphs for real-company products
(AWS Lambda → rocket, Datadog → dog, Stripe → card…) looks
random and slightly off-brand. We already shipped one-off
custom icons for Claude Code + Codex that the user actually
liked. Want to generalize.

Shape: user drops an image (PNG/SVG) on the rail/integrations
overlay or runs `:icon.create <integration-id>`. We:
  1. Open a designer pane that lets the user crop, threshold,
     and scale the image into a 16×16 / 24×24 monochrome grid
     (one cell per terminal cell).
  2. Either embed it directly as a Sixel/Kitty graphics fragment
     in the rail row (when the terminal supports it — we already
     have this code path for image preview) OR encode it as a
     custom-glyph entry in `~/.config/mnml/icons/` that the rail
     renderer prefers over the Nerd Font fallback.

Why deferred: needs decisions on (a) image → glyph algorithm
(otsu threshold? edge-detect? user-painted?), (b) rendering
substrate (Sixel vs. Kitty vs. precomputed font slots), and
(c) how the user-stored icon overrides the catalog default.
Don't build until shape is settled — a half-done image flow
is worse than just keeping the Nerd Font icons.

Pick up when the user has a few hours and an integration whose
current glyph annoys them enough to drive the design.

### ~~HTTP: drop non-dirty "new request" tab on navigate-away~~
**Shipped 2026-07-09.** `set_activity_section` in `layout.rs`
now marks the auto-opened Request pane `is_preview = true`
and closes any preview Request pane on leaving HTTP. First
edit promotes to a permanent tab (existing
`tui/handlers/pane.rs` promotion path handles this). 3 unit
tests (`entering_http_opens_...`, `leaving_http_drops_...`,
`leaving_http_keeps_promoted_...`).

### ~~TODOs panel: 1-cell padding between rescan icon and label~~
**Verified stale 2026-07-09.** Audit: `todos_panel.rs:266`
renders `"⟳  Rescan"` with a 2-space gap (intentional —
comment on-site explains `⟳` eats its right sidebearing in
Nerd Font). HTTP panel refresh chips use icon-only codicons
(no adjacent label to kiss). Notes panel has no Rescan chip.
No `⟳` + adjacent word without a gap remains in the tree.

### ~~TODOs panel: add `/` filter row (parity with HTTP / Agents)~~
**Shipped 2026-07-09.** `todos_panel_filter` state + filter row
render + `/`-focus keybinding + click-to-focus + click-outside-
to-unfocus. Header shows `(N of M hits)` when active. Matches
tag, path, or title case-insensitively.

### ~~TODOs panel: pick up `.fail` markers in Playwright workspaces~~
**Shipped 2026-07-09.** Playwright test files (`.spec.ts` /
`.test.ts` / `.spec.js` / `.test.js`) now get an extra scanner
pass that picks up `.fixme(` / `.fail(` / `.skip(` call-site
tokens. Mapped to FIXME / XXX / REVIEW respectively; the test
title (first quoted string in the call) becomes the entry
text. Non-test files still use the comment-only path.

### ~~Notes panel: add `/` filter row~~
**Shipped 2026-07-09.** Same idiom as TODOs above — filter row,
`/` to focus, Esc clears + unfocuses. Matches note file names
case-insensitively. Header shows `(N of M)` when active.

### ~~Notes panel: `+ New note` chip is a no-op~~
**Verified 2026-07-09.** Handler works — creates
`.mnml/notes/note-N.md`, refreshes the panel, opens as an
MdPreview pane. Locked in by 2 unit tests
(`notes_new_note_tests` in `src/app/workspace_methods.rs`).
The original report was stale; whatever regressed it was fixed
in a prior session and no follow-up was needed.

### ~~Tab bar: add Claude Code + Codex launcher icons~~
**Shipped 2026-07-09.** Bumped the `[ui] tab_bar_ai_icon`
default from `"none"` to `"both"` — every leaf's tab strip now
shows Claude Code + Codex chips left of the terminal glyph:
`[Claude][Codex][$][⊟][⊞]`. Chips were already implemented
and the click handler already routes to
`ai.claude_code_new` / `ai.codex_new`; only the default
needed flipping. Users can still opt out with `tab_bar_ai_icon
= "none"` (or `"claude_code"` / `"codex"` for one only).

### ~~Tab bar: right-click on AI launcher chip → placement menu~~
**Status:** captured 2026-07-08 user request.

Extend the far-right cluster of icons on the per-leaf tab
strip (currently `$` terminal / `⊟` split-vert / `⊞`
split-horiz) with two more chips positioned IMMEDIATELY LEFT
of the terminal glyph:

  [ Claude Code ] [ Codex ] [ $ ] [ ⊟ ] [ ⊞ ]

Click → spawn a fresh Claude Code / Codex session (mirrors
existing `ai.claude_code_new` / `ai.codex_new` commands) in
the current leaf.

Reuse the existing `tab_bar_ai_icon` config knob that already
gates `"claude_code"` / `"codex"` / `"both"` variants — but
this is asking to make BOTH visible unconditionally on the
tab bar, i.e. bump the default from `"none"` (current) to
`"both"`, or add a new `[ui] tab_bar_ai_launchers = true`
switch.

**Shipped 2026-07-09 (halves only).** Right-click on either
tab-strip AI chip opens a context menu:

    Claude Code launcher
      Open new Claude Code session (right dock)
      Toggle existing Claude Code pane
      Place new session in left half
      Place new session in right half
      Place new session in top half
      Place new session in bottom half

The chip's Claude/Codex kind is picked up from the `tag` field
already on `split_strip_ai_buttons`; the menu is symmetric.

New `crate::app::ai::PanePlacement` enum + `open_*_at`
methods. 8 palette commands registered
(`ai.claude_code_new_left/right/top/bottom`, ditto Codex) so
users can bind them to chords too.

**Deferred: quarters** (Top-left / Top-right / Bottom-left /
Bottom-right). Would need recursive split (horizontal split →
then vertical split on the chosen half → then move the new
pane into position). Not enough demand vs. drag-drop parity to
land in this pass.

### HTTP: dynamic + realistic request generation (roadmap)
**Status:** roadmap captured 2026-07-09. User wants stubs that
are "as dynamic and realistic as possible" — not just cleaner
sync diffs but genuinely usable ready-to-fire scaffolds. A
`↻ Regenerate` button on the Request pane rerolls example data
for repeated sends without manual editing.

**Tier 1 — dynamic value substitution (deterministic sync).**
`--normalize` flag on `mnml discover` / `mnml sync` /
`mnml sync-check` + palette `[http] sync_normalize = true`.
When on:
- ISO 8601 timestamp strings → `{{$timestamp}}`.
- Lowercase UUIDs → `{{$uuid}}`.
- Numeric ID fields where the name suggests randomness
  (`orderId` as int, `requestId`) → `{{$randomInt}}`.
Not substituted: date-only strings, epoch integers, uppercase
UUIDs — too many false positives.
Impact: swagger-side timestamp/UUID churn (117 files/sync on
tattle-mnml-workspace) drops to zero.

**Tier 2 — property-name-keyed faker vocab.** Small
`src/http/faker.rs` module. When schema-synthesis has a
`type: "string"` field with no example, look up the property
name and emit a realistic value instead of `"string"`:
- `firstName`/`givenName``"John"`
- `lastName`/`familyName`/`surname``"Smith"`
- `emailAddress`/`email``"user@example.com"`
- `phoneNumber`/`phone``"555-0100"`
- `city``"San Francisco"`
- `zipCode`/`postalCode``"94105"`
- `countryCode``"US"`
- `currency``"USD"`
- `merchantId`/`userId`/`accountId``{{MERCHANT_ID}}` etc
  (env-var references, not literal ints)
Similar heuristics for integer / enum types:
- `quantity` / `count``1`
- `price` / `amount``9.99`
- Enum: prefer "active"-like values over "deactivated" /
  "archived" / "cancelled" (skip words with `-ed` suffix
  and negative prefixes when possible).

**Tier 3 — coherent object graphs.** Cross-field consistency
within a single body:
- `firstName` + `lastName` on the same object → matched pair
  ("John" + "Smith", not two independent picks).
- `orderPlacedUtc` + `orderCompletedUtc` → picked ~30 min apart.
- `amount` + `currency` + `total` → math still works out.
- `email` matches `firstName.lastName@example.com` when both
  are on the same object.

**Tier 4 — well-known env-var relations.** Cross-request
consistency:
- Path params `{{merchantId}}` in URL, body, query all map to
  the same `MERCHANT_ID` env var (not three independent
  templates).
- Ship `.mnml/env/dev.env.example` with the common vars
  pre-seeded: `MERCHANT_ID=`, `USER_ID=`, `LOCATION_ID=`,
  `BASE_URL=`.

**Tier 5 — query params + headers from swagger `parameters`.**
Currently only path params are templated; query and header
params from swagger are silently dropped.
- Required query params → `?filter={{filter}}` appended to URL.
- Optional query params → commented-out `# ?filter=<value>`
  hint below the curl line.
- Header params → `-H '<name>: <example-or-template>'` in the
  header block.

**Tier 6 — auto chain generation + extract hints.**
- `POST /*/auth/login` stubs get a comment
  `# extract: TOKEN=$.access_token` documenting how a chain
  should pull the token.
- Endpoints returning `{ id: ... }` in the response schema
  get `# extract: LAST_ID=$.id`.
- Per-tag starter chains: `.mnml/chains/orders.chain.json`
  auto-generated as `login → create → get → update → delete`
  when the API surface has all four verbs on a resource.

**Tier 7 — happy-path + edge-case variety.** Optional flag
`--edge-cases` generates a `<base>.happy.curl` +
`<base>.edge.curl` pair for each operation. Happy = default
faker values. Edge = empty strings, min/max values, boundary
enum values. Skip unless requested.

**`↻ Regenerate` button on the Request pane.** Companion to
tiers 1-3. Chip on the Request block header (near `{ } Format`
and `↺ Refresh`). Click → walks the body, finds anything that
LOOKS like a dynamic value we could have generated (via the
same detection Tier 1 uses), and rerolls with fresh randoms.

Rules:
- Timestamps: new `now()` UTC.
- UUIDs (lowercase, standard shape): new `uuid_v4()`.
- Faker-vocab strings (matched against the small dictionary
  from Tier 2): pick a new value from the same category the
  original was in. E.g., `"John"` gets replaced by another
  first name, not by a city.
- Numeric IDs the tier-1 substitution would have caught: new
  randints in the same range.
- Do NOT touch: strings that don't match any known pattern
  (probably user-authored), literal `{{$uuid}}` / `{{$timestamp}}`
  templates already in place (those resolve at send anyway),
  path parameters, headers.

Right-click on the chip → menu:
- Regenerate all (default click behavior)
- Regenerate timestamps only
- Regenerate UUIDs only
- Regenerate faker fields only
- Convert to `{{$dynamic}}` templates (opposite direction —
  turn the concrete values back into placeholders so every
  send is fresh without a click).

Palette: `http.regenerate_body`, `http.convert_to_dynamic`.

**Practical sequence (recommended):**
1. Tier 1 — small, unblocks clean git history immediately.
2. Tier 5 — real feature gap; query/header params make stubs
   actually usable.
3. Tier 2 — big usability jump; scoped to one new module.
4. Regenerate button — depends on Tier 1 detection rules.
5. Tier 4 — cross-request consistency; env-var convention.
6. Tier 6 — auto chains; where mnml gets ahead of Postman/Bruno.
7. Tier 3 — polish on top of Tier 2.
8. Tier 7 — diminishing returns; skip unless requested.

### Activity sidebar: add / remove integrations from the UI + right-click menus
**Status:** captured 2026-07-09 user request. "todo add integrations
to the activity sidebar and removing. should probably also be in
right click menus to edit or remove. iterate as much as needed i
dont want to lose functionality."

Current shape:
- Integrations live in `.mnml/integrations/<id>.toml` (workspace)
  or `~/.config/mnml/integrations/<id>.toml` (user).
- The Integrations section on the activity sidebar reads
  `App::config.ui.integration_icons` at render time.
- Adding one today: manually author the TOML, then
  `:integrations.refresh` to pick it up.
- Removing: `:integration.uninstall <id>` OR delete the file
  and refresh.

What's missing (surface-level ergonomics):

**1. `+ Add integration` chip at the bottom of the section.**
Click → opens the marketplace picker (fuzzy over
`family_catalog::CATALOG` + community manifests). Accept →
downloads the sibling manifest, writes the TOML, refreshes.
Same install flow as `:sibling.install` but reachable without
palette-hunting.

**2. Right-click menu on any Integration chip.**
- Edit config (opens `<id>.toml` in an editor pane)
- Toggle enabled (mirrors `:integration.toggle_enabled`)
- Uninstall (with confirm)
- Copy id to clipboard
- View sibling repo (if the manifest has a `homepage` field)
- Show in Finder (macOS) / File Manager

**3. `- Remove` chip visible on hover.**
Same idiom as HTTP-panel CAPTURED clear chip — an `×` that
appears on the chip when the mouse is over it. Click → confirm
prompt → remove.

**4. Reorder via drag.**
Chips accept vertical drag-drop; the ordering persists to a
new `[ui] integration_order = ["id1","id2",...]` config key.
Users who install 15 siblings currently can't reorder the
common ones to the top.

**5. Import from URL / paste manifest.**
`+ Add integration` menu also offers "paste manifest…" — a
prompt where the user pastes a TOML block. Saves to
`.mnml/integrations/<id>.toml` after basic validation.
Same shape as `+ New env` / `+ New chain` prompts already in
the HTTP panel.

Non-goals (deliberately):
- In-app manifest EDITOR (grid / form). Config TOML in an editor
  pane is fine — text is the canonical source.
- Marketplace ratings / reviews. Local package management, not
  a curation portal.

Preserve functionality — the palette commands stay reachable
(`:integration.install`, `:integration.uninstall`,
`:integration.toggle_enabled`, `:integrations.refresh`) so
scripts and IPC callers don't break. UI adds are additive.

### HTTP: discover should expand `requestBody.content.*.examples` (map)
**Status: DONE 2026-07-09** — shipped in the same session.
`src/http/discover.rs` now handles `.examples` map expansion +
schema synthesis (ports both from `archived/rqst/src/discover.rs`).
Tests added:
`named_examples_map_expands_to_one_file_per_example`,
`schema_synthesis_fills_body_when_no_example_provided`,
`schema_synthesis_resolves_local_refs_and_survives_cycles`.

Historical note (why the entry existed):

Current: `src/http/discover.rs` handles `requestBody.content.
"application/json".example` (SINGULAR — one example → filled
into the stub body). Explicitly does NOT handle the plural
`examples` (map of named examples).

Impact on tattle workspace:
- `tasks-api` swagger: 2 real operations, but `POST /admin/event`
  has 219 named `examples`. Old rqst tool expanded that map into
  203 `TriggerEvent.<eventName>.curl` files (one per event
  type). Users navigated by event name via `Ctrl+O`.
- `integrations-api`: 247 operations, 1 with a 133-entry
  `examples` map. Existing 372 stubs = 247 + ~125 example
  expansions. New discover generates 247, so ~125 are
  effectively lost.

Fix shape:
- After emitting the "one stub per operation" default,
  iterate `requestBody.content.<mime>.examples` for each
  operation.
- For each `(name, example)` pair:
  - Emit an extra stub at `<tag>/<operationId>.<name>.curl`
    (or nested subdir `<tag>/<operationId>/<name>.curl`).
  - Fill the body with the named example's `.value` field.
- Add an `Options.expand_examples: bool` flag (default `true`)
  so users can opt out for surface-only discover.
- README the new file-count expectation.

Naming clash: existing tattle stubs use `TriggerEvent.<name>.curl`
where TriggerEvent is the operationId. That's the same pattern
the new expansion should produce. Keeps 1:1 filename parity
with the old rqst tool → `mnml sync-check` becomes a clean
"real drift" report on tattle workspaces.

Related: `mnml sync-check` (shipped 7b3dff9 / 4f3c4f6) currently
misreports 1959 files of drift on tattle-mnml-workspace almost
entirely because of this feature regression. Once expansion is
back, the drift number will collapse to actual API changes.

### ~~HTTP: "Generate AI prompt" button on failed requests~~
**Shipped 2026-07-09.** `⚡ AI` chip on the Response tab strip
(only when the response is a failure — non-2xx status,
schema-invalid, or transport error) copies a structured
markdown prompt to the system clipboard. New palette command
`http.copy_ai_prompt` for the same action. Hover tooltip
explains the redaction. Prompt structure:

    ## Request
    METHOD URL
    Headers (Authorization / *api-key* / Cookie / *secret*
      redacted; scheme kept: `Bearer <redacted>`)
    Body (truncated to 2KB with a marker if longer)

    ## Response
    HTTP <status>  (elapsed: <ms>ms)
    Headers + Body (2KB cap)

    ## Env / context
    - active env: <name>
    - defined vars used: …
    - undefined vars: …

    ## Schema validation
    - <errors>

    ## What I've tried
    (fill me in)

New module `src/http/ai_prompt.rs` — 8 unit tests (redaction
variants, truncation, env-var classification, failure state
detection). Right-click menu variants (Open in AI pane,
Save to `.mnml/ai-prompts/`) not shipped yet — plain click
covers the 90% case; extend later if the need surfaces.

Original spec preserved:

When a Request pane's response is a failure — non-2xx status,
schema validation error, connection error, timeout — surface a
one-click chip that copies (or opens) an AI-ready prompt with
all the details a coding assistant would need to help debug.

Shape of the generated prompt:

    I'm hitting an error on this HTTP request. Help me figure
    out why.

    ## Request
    METHOD URL
    (headers, with sensitive values redacted)
    (body)

    ## Response
    HTTP <status>  (elapsed: <ms>ms)
    (headers)
    (body — pretty-printed if JSON, capped at ~2KB with a
    `…truncated` marker)

    ## Env / context
    - active env: <name>
    - defined vars used: <list of {{VARS}} that were substituted>
    - undefined vars: <list of {{VARS}} that stayed literal>

    ## Schema validation (if a .schema.json sidecar exists)
    <the errors from schema_result>

    ## What I've tried
    (blank — for user to fill in)

Where the button lives:
- Response block header, right of the `copy` / `wrap` chips.
- Only shown when `RunState::Done` has status ∉ [200, 300) OR
  `schema_result` has errors OR `RunState::Failed`.
- Label: `⚡ AI` or `? AI prompt` (nerd glyph + short label).

Two flavors of click:
1. **Plain click** — copy the prompt to the clipboard, toast
   "prompt copied — paste into Claude / Codex".
2. **Right-click** — menu:
   - Copy to clipboard
   - Open in a new AI pane (spawns `ai.claude_code_new` /
     `ai.codex_new` with the prompt pre-filled)
   - Save as `.mnml/ai-prompts/<timestamp>-<method>-<url-slug>.md`

Also add ex/palette variants:
- `http.copy_ai_prompt` (palette)
- `http.ask_ai_about_failure` (opens AI pane with prompt)

Sensitive-value redaction: any header whose key matches
`(?i)authorization|api[_-]?key|token|cookie|x-.*-secret` gets
its value replaced with `<redacted>`. Same rule for body fields
that LOOK like tokens (Bearer-length strings, JWT shape).

Reason this beats "just paste the whole tab": the prompt is
structured for AI parsing, keeps the important context, and
strips secrets so users don't leak credentials into their AI
provider by accident.