Skip to content

feat(ui): redesign Create New Order tab with picker and leave guard#93

Open
arkanoider wants to merge 2 commits into
mainfrom
feat/new-order-form-ux
Open

feat(ui): redesign Create New Order tab with picker and leave guard#93
arkanoider wants to merge 2 commits into
mainfrom
feat/new-order-form-ux

Conversation

@arkanoider

@arkanoider arkanoider commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Redesigns the Create New Order tab for clearer layout, better use of space, and safer navigation away from in-progress drafts.

  • Sectioned form (TRADE / PRICING / TERMS) with compact input strips, focus arrow (), inline ✓/✗ validation, and green section headers when complete
  • Live preview receipt card with readiness status and implied BTC price when computable
  • Searchable currency picker (src/ui/currencies.rs): prefers MostroInstanceInfo.fiat_currencies_accepted, falls back to bundled ISO-4217 list; bold code + dim name (no emoji flags — poor terminal font support)
  • Leave guard (ConfirmLeaveOrder): Left/Right tab navigation on a dirty form prompts Keep editing vs Leave; draft stored in AppState.order_form_draft and restored on return
  • UX fixes: Space inserts text in Payment Method; buy/sell hint changed to ⇄ Space; tab auto-restores a usable form instead of a dead placeholder
  • Docs: docs/TUI_INTERFACE.md and docs/README.md updated for AI/contributor context

Motivation

The previous form left large empty areas, used free-text currency entry, and lost partial input when users switched tabs. This brings the tab in line with the rest of the TUI polish (My Trades, Messages) and instance-aware currency selection.

Breaking changes

None — order submission protocol and FormState field semantics are unchanged.

Checklist

  • Feature: sectioned New Order form + currency picker + leave guard
  • Bug fix: payment method spaces; misleading order-type navigation hint
  • Unit tests pass (cargo test — 157 tests)
  • cargo fmt, cargo clippy --all-targets --all-features, cargo build

Test plan

  • Open Create New Order — verify sectioned layout, preview card, field help strip
  • Currency field: open picker (Enter/Space/type), filter list, select code; confirm bold code + name in field and preview
  • Toggle buy/sell (Space on Type) and single/range (Space on Fiat)
  • Type a payment method with spaces (e.g. SEPA Instant)
  • Fill partial form, press Left/Right — confirm leave guard; Keep editing returns; Leave preserves draft when returning to tab
  • Complete form, submit — confirm existing YES/NO confirmation flow still works
  • Esc on form clears draft; empty navigation leaves without prompt

Summary by CodeRabbit

  • New Features

    • Added a richer “Create New Order” experience with a sectioned form, live preview, inline validation, and a searchable currency picker.
    • Drafted order details are now preserved when switching tabs, so users can resume editing later.
  • Bug Fixes

    • Added a confirmation prompt before leaving an unfinished order, helping prevent accidental loss of entered details.
    • Improved keyboard behavior for the order form, including better navigation, selection, and cancel/submit handling.

Replace the sparse stacked-field form with a sectioned layout, live
preview receipt, inline validation, and a searchable currency dropdown
backed by instance fiat_currencies_accepted or a bundled ISO list.
Add draft persistence and a confirm-on-leave popup when switching tabs
with unsaved input; allow spaces in payment method descriptions.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f084173f-e12c-4252-a655-0a75f29d93a7

📥 Commits

Reviewing files that changed from the base of the PR and between 3d45a03 and babc9bc.

📒 Files selected for processing (3)
  • docs/TUI_INTERFACE.md
  • src/ui/currencies.rs
  • src/ui/order_form.rs
👮 Files not reviewed due to content moderation or server errors (3)
  • src/ui/currencies.rs
  • docs/TUI_INTERFACE.md
  • src/ui/order_form.rs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main UI redesign and the new picker/leave-guard behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/new-order-form-ux

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/ui/currencies.rs (1)

251-272: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider deduplicating/filtering blank codes in resolve_options.

If accepted contains duplicate or empty codes (e.g. malformed relay data), the resulting option list will contain duplicate or blank entries in the picker dropdown.

♻️ Suggested defensive fix
 pub fn resolve_options(accepted: &[String]) -> Vec<CurrencyOption> {
     if accepted.is_empty() {
         return CURRENCIES
             .iter()
             .map(|c| CurrencyOption {
                 code: c.code.to_string(),
                 name: c.name.to_string(),
             })
             .collect();
     }

-    accepted
+    let mut seen = std::collections::HashSet::new();
+    accepted
         .iter()
-        .map(|code| {
-            let upper = code.trim().to_ascii_uppercase();
-            CurrencyOption {
-                name: name_for(&upper).to_string(),
-                code: upper,
-            }
-        })
+        .filter_map(|code| {
+            let upper = code.trim().to_ascii_uppercase();
+            if upper.is_empty() || !seen.insert(upper.clone()) {
+                return None;
+            }
+            Some(CurrencyOption {
+                name: name_for(&upper).to_string(),
+                code: upper,
+            })
+        })
         .collect()
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/currencies.rs` around lines 251 - 272, resolve_options currently maps
every accepted entry directly into a CurrencyOption, so duplicate or blank
inputs can produce duplicate or empty picker items. Update resolve_options to
defensively normalize each code, skip entries that trim to empty, and
deduplicate by the uppercased currency code before collecting. Keep the behavior
for the empty-accepted case unchanged, and make sure the filtering happens in
the resolve_options path using name_for and CurrencyOption.
src/ui/key_handler/navigation.rs (1)

505-539: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding unit tests for the new draft/leave-guard state machine.

restore_or_new_form, handle_form_leave_attempt, and leave_creating_order_to_adjacent_tab introduce non-trivial branching (dirty vs. clean form, direction, draft take/restore) split across this file plus esc_handlers.rs/confirmation.rs/enter_handlers.rs. A few focused unit tests around handle_form_leave_attempt (dirty → ConfirmLeaveOrder, clean → immediate leave with draft cleared) would guard against regressions as this flow evolves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/key_handler/navigation.rs` around lines 505 - 539, Add focused unit
tests for the new draft/leave-guard flow around restore_or_new_form,
handle_form_leave_attempt, and leave_creating_order_to_adjacent_tab. Cover the
key branches: dirty form should switch AppState.mode to
UserMode::ConfirmLeaveOrder with the form preserved, clean form should clear
order_form_draft and immediately call leave_creating_order_to_adjacent_tab, and
restore_or_new_form should consume a saved draft via take() or fall back to
FormState::new_default_form. Use these symbols in the tests so the branching
behavior stays covered even if surrounding handlers in esc_handlers.rs,
confirmation.rs, or enter_handlers.rs change.
src/ui/key_handler/form_input.rs (1)

15-121: 📐 Maintainability & Code Quality | 🔵 Trivial

Consider adding unit tests for the new picker state machine.

handle_currency_picker_key, open_currency_picker, and close_currency_picker introduce non-trivial branching (open/close, filter mutation, wraparound Up/Down index math, selection clamping on filter change) with no accompanying tests in this file, unlike the existing is_creating_order_text_input test below it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/key_handler/form_input.rs` around lines 15 - 121, Add unit tests for
the new picker state machine around handle_currency_picker_key,
open_currency_picker, and close_currency_picker. Cover closed-vs-open behavior,
opening via Enter/Space/typing, filter mutation and reset, Up/Down wraparound,
Enter selection clamping after filtering, and Esc closing/resetting state. Use
the existing CreatingOrder/FormState/AppState paths and mirror the style of the
is_creating_order_text_input test below it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/TUI_INTERFACE.md`:
- Around line 345-357: Add a language specifier to the fenced code block in the
“Key files for codegen” section to satisfy MD040; update the markdown fence
around the file list in docs/TUI_INTERFACE.md to include a language tag such as
text, keeping the contents and surrounding documentation unchanged.

In `@src/ui/order_form.rs`:
- Around line 352-452: The focused cursor position is derived from Row.text_len
in render_details, but Amount, Premium, and Expiry are rendered with extra
decoration from amount_val, premium_line, and expiry_line so the caret no longer
matches the visible text. Update the Row construction in order_form.rs so these
fields show the raw editable string whenever form.focused is
FormField::AmountSats, FormField::Premium, or FormField::ExpirationDays, and
keep the decorated display only when unfocused. This will make text_len align
with what is actually rendered and fix the cursor placement.
- Around line 651-732: The popup sizing in the currency selection render path
can leave the empty-state message with no space when filtered is empty. Update
the sizing logic in the render block around the popup Rect/Layout split so the
inner area always reserves enough rows for both the "no match" text and the
hint, and make the height calculation in the currency popup rendering robust
against the zero-item case. Use the existing symbols filtered, max_rows, popup,
and the [Constraint::Min(1), Constraint::Length(1)] split to locate the fix.

---

Nitpick comments:
In `@src/ui/currencies.rs`:
- Around line 251-272: resolve_options currently maps every accepted entry
directly into a CurrencyOption, so duplicate or blank inputs can produce
duplicate or empty picker items. Update resolve_options to defensively normalize
each code, skip entries that trim to empty, and deduplicate by the uppercased
currency code before collecting. Keep the behavior for the empty-accepted case
unchanged, and make sure the filtering happens in the resolve_options path using
name_for and CurrencyOption.

In `@src/ui/key_handler/form_input.rs`:
- Around line 15-121: Add unit tests for the new picker state machine around
handle_currency_picker_key, open_currency_picker, and close_currency_picker.
Cover closed-vs-open behavior, opening via Enter/Space/typing, filter mutation
and reset, Up/Down wraparound, Enter selection clamping after filtering, and Esc
closing/resetting state. Use the existing CreatingOrder/FormState/AppState paths
and mirror the style of the is_creating_order_text_input test below it.

In `@src/ui/key_handler/navigation.rs`:
- Around line 505-539: Add focused unit tests for the new draft/leave-guard flow
around restore_or_new_form, handle_form_leave_attempt, and
leave_creating_order_to_adjacent_tab. Cover the key branches: dirty form should
switch AppState.mode to UserMode::ConfirmLeaveOrder with the form preserved,
clean form should clear order_form_draft and immediately call
leave_creating_order_to_adjacent_tab, and restore_or_new_form should consume a
saved draft via take() or fall back to FormState::new_default_form. Use these
symbols in the tests so the branching behavior stays covered even if surrounding
handlers in esc_handlers.rs, confirmation.rs, or enter_handlers.rs change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4af1b141-fa7d-4019-bfb9-4ef26022e8c9

📥 Commits

Reviewing files that changed from the base of the PR and between c314469 and 3d45a03.

📒 Files selected for processing (16)
  • docs/README.md
  • docs/TUI_INTERFACE.md
  • src/ui/app_state.rs
  • src/ui/currencies.rs
  • src/ui/draw.rs
  • src/ui/key_handler/confirmation.rs
  • src/ui/key_handler/enter_handlers.rs
  • src/ui/key_handler/esc_handlers.rs
  • src/ui/key_handler/form_input.rs
  • src/ui/key_handler/mod.rs
  • src/ui/key_handler/navigation.rs
  • src/ui/mod.rs
  • src/ui/order_confirm.rs
  • src/ui/order_form.rs
  • src/ui/orders.rs
  • src/ui/user_state.rs

Comment thread docs/TUI_INTERFACE.md
Comment on lines +345 to +357
#### Key files for codegen

```
src/ui/order_form.rs # render_order_form, validation, preview
src/ui/currencies.rs # ISO list + resolve/filter helpers
src/ui/orders.rs # FormState, FormField, CurrencyPicker, is_dirty
src/ui/order_confirm.rs # render_order_confirm, render_leave_confirm
src/ui/draw.rs # tab render + ConfirmLeaveOrder overlay
src/ui/key_handler/form_input.rs # char/backspace + handle_currency_picker_key
src/ui/key_handler/navigation.rs # tab leave guard, restore_or_new_form
src/ui/key_handler/enter_handlers.rs # ConfirmingOrder / ConfirmLeaveOrder Enter
src/ui/key_handler/esc_handlers.rs # Esc on form / leave guard
```

@coderabbitai coderabbitai Bot Jul 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language to the fenced code block (MD040).

Static analysis flags this fence for missing a language specifier.

📝 Suggested fix
-```
+```text
 src/ui/order_form.rs          # render_order_form, validation, preview
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#### Key files for codegen
```
src/ui/order_form.rs # render_order_form, validation, preview
src/ui/currencies.rs # ISO list + resolve/filter helpers
src/ui/orders.rs # FormState, FormField, CurrencyPicker, is_dirty
src/ui/order_confirm.rs # render_order_confirm, render_leave_confirm
src/ui/draw.rs # tab render + ConfirmLeaveOrder overlay
src/ui/key_handler/form_input.rs # char/backspace + handle_currency_picker_key
src/ui/key_handler/navigation.rs # tab leave guard, restore_or_new_form
src/ui/key_handler/enter_handlers.rs # ConfirmingOrder / ConfirmLeaveOrder Enter
src/ui/key_handler/esc_handlers.rs # Esc on form / leave guard
```
#### Key files for codegen
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 347-347: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/TUI_INTERFACE.md` around lines 345 - 357, Add a language specifier to
the fenced code block in the “Key files for codegen” section to satisfy MD040;
update the markdown fence around the file list in docs/TUI_INTERFACE.md to
include a language tag such as text, keeping the contents and surrounding
documentation unchanged.

Source: Linters/SAST tools

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, something went wrong! Please try again later. 🐰 💔

Comment thread src/ui/order_form.rs Outdated
Comment on lines +352 to +452
let amount_val = if form.amount.trim() == "0" || form.amount.trim().is_empty() {
Line::from(Span::styled(
"market",
Style::default()
.fg(Color::DarkGray)
.add_modifier(Modifier::ITALIC),
))
} else {
Line::from(format!("{} sats", group_thousands(&form.amount)))
};

let mut rows = vec![
Row {
field: FormField::OrderType,
label: "Type",
value: type_val,
prefix_len: 0,
text_len: 0,
editable: false,
},
Row {
field: FormField::Currency,
label: "Currency",
value: currency_val,
prefix_len: 0,
text_len: 0,
editable: false,
},
Row {
field: FormField::AmountSats,
label: "Amount",
value: amount_val,
prefix_len: 0,
text_len: form.amount.len(),
editable: true,
},
];

let (tag, tag_color) = if form.use_range {
("[Range] ", Color::Magenta)
} else {
("[Single] ", Color::Cyan)
};
rows.push(Row {
field: FormField::FiatAmount,
label: if form.use_range { "Fiat min" } else { "Fiat" },
value: Line::from(vec![
Span::styled(
tag,
Style::default().fg(tag_color).add_modifier(Modifier::BOLD),
),
Span::raw(&form.fiat_amount),
])
Span::raw(form.fiat_amount.clone()),
]),
prefix_len: tag.chars().count(),
text_len: form.fiat_amount.len(),
editable: true,
});

if form.use_range {
rows.push(Row {
field: FormField::FiatAmountMax,
label: "Fiat max",
value: Line::from(form.fiat_amount_max.clone()),
prefix_len: 0,
text_len: form.fiat_amount_max.len(),
editable: true,
});
}

rows.push(Row {
field: FormField::PaymentMethod,
label: "Method",
value: dim_if_empty(&form.payment_method, "(any)"),
prefix_len: 0,
text_len: form.payment_method.len(),
editable: true,
});
rows.push(Row {
field: FormField::Premium,
label: "Premium",
value: premium_line(&form.premium),
prefix_len: 0,
text_len: form.premium.len(),
editable: true,
});
rows.push(Row {
field: FormField::Invoice,
label: "Invoice",
value: dim_if_empty(&form.invoice, "(optional)"),
prefix_len: 0,
text_len: form.invoice.len(),
editable: true,
});
rows.push(Row {
field: FormField::ExpirationDays,
label: "Expiry",
value: expiry_line(&form.expiration_days),
prefix_len: 0,
text_len: form.expiration_days.len(),
editable: true,
});

@coderabbitai coderabbitai Bot Jul 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Cursor misaligns with the visible text for Amount, Premium, and Expiry fields.

Row.text_len uses the raw string length, but the rendered value for these three rows is decorated (thousands separators + " sats", "+N %", "N day(s)"/"no expiry"). The cursor (VALUE_OFFSET + prefix_len + text_len, set in render_details) will visually land inside the decorated text rather than at the true edit position — e.g. editing Premium with raw "12" renders "+12 %", but the cursor sits after only 2 characters into that string.

Editing itself still works (typing/backspace always operate on the raw string), but the visible caret position is misleading whenever these three fields are focused.

🐛 Suggested approach: show raw text while the field is focused
-    let amount_val = if form.amount.trim() == "0" || form.amount.trim().is_empty() {
-        Line::from(Span::styled(
-            "market",
-            Style::default()
-                .fg(Color::DarkGray)
-                .add_modifier(Modifier::ITALIC),
-        ))
-    } else {
-        Line::from(format!("{} sats", group_thousands(&form.amount)))
-    };
+    let amount_val = if form.focused == FormField::AmountSats {
+        Line::from(form.amount.clone())
+    } else if form.amount.trim() == "0" || form.amount.trim().is_empty() {
+        Line::from(Span::styled(
+            "market",
+            Style::default()
+                .fg(Color::DarkGray)
+                .add_modifier(Modifier::ITALIC),
+        ))
+    } else {
+        Line::from(format!("{} sats", group_thousands(&form.amount)))
+    };

Apply the same form.focused == FormField::… gate to premium_line(&form.premium) and expiry_line(&form.expiration_days) call sites so text_len always matches what's on screen while editing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/order_form.rs` around lines 352 - 452, The focused cursor position is
derived from Row.text_len in render_details, but Amount, Premium, and Expiry are
rendered with extra decoration from amount_val, premium_line, and expiry_line so
the caret no longer matches the visible text. Update the Row construction in
order_form.rs so these fields show the raw editable string whenever form.focused
is FormField::AmountSats, FormField::Premium, or FormField::ExpirationDays, and
keep the decorated display only when unfocused. This will make text_len align
with what is actually rendered and fix the cursor placement.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, something went wrong! Please try again later. 🐰 💔

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, something went wrong! Please try again later. 🐰 💔

Comment thread src/ui/order_form.rs Outdated
Comment on lines +651 to +732
let max_rows = filtered.len().min(8) as u16;
let height = max_rows + 3; // border (2) + hint (1)
let mut y = anchor.y + 1;
if y + height > bounds.y + bounds.height {
y = anchor.y.saturating_sub(height);
}
let popup = Rect {
x,
y,
width,
height: height.min(bounds.y + bounds.height - y),
};

f.render_widget(Clear, popup);

let title = if currencies.is_empty() {
format!(" Select currency ({} common) ", options.len())
} else {
format!(" Select currency ({} accepted) ", options.len())
};
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.style(Style::default().bg(BACKGROUND_COLOR).fg(PRIMARY_COLOR));
let inner = block.inner(popup);
f.render_widget(block, popup);

let split = Layout::new(
Direction::Vertical,
[Constraint::Min(1), Constraint::Length(1)],
)
.split(inner);

if filtered.is_empty() {
f.render_widget(
Paragraph::new(Span::styled(
" no match",
Style::default().fg(Color::DarkGray),
))
.style(Style::default().bg(BACKGROUND_COLOR)),
split[0],
);
} else {
let items: Vec<ListItem> = filtered
.iter()
.map(|o| {
let mut spans = vec![Span::styled(
format!("{:<5}", o.code),
Style::default().add_modifier(Modifier::BOLD),
)];
if !o.name.is_empty() {
spans.push(Span::styled(
o.name.clone(),
Style::default().add_modifier(Modifier::DIM),
));
}
ListItem::new(Line::from(spans))
})
.collect();

let list = List::new(items)
.style(Style::default().fg(Color::White).bg(BACKGROUND_COLOR))
.highlight_style(
Style::default()
.fg(Color::Black)
.bg(PRIMARY_COLOR)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("› ");
let mut state = ListState::default().with_selected(Some(selected));
f.render_stateful_widget(list, split[0], &mut state);

if filtered.len() > split[0].height as usize {
let mut sb_state = ScrollbarState::new(filtered.len()).position(selected);
f.render_stateful_widget(
Scrollbar::default().orientation(ScrollbarOrientation::VerticalRight),
split[0],
&mut sb_state,
);
}
}

@coderabbitai coderabbitai Bot Jul 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Empty-filter popup may starve the "no match" line of height.

height = max_rows + 3 (border 2 + hint 1) yields height = 3 when filtered is empty (max_rows = 0), leaving only 1 inner row for a [Min(1), Length(1)] split that needs 2 rows (no-match text + hint). The "no match" line can end up with 0 height depending on how the layout solver resolves the shortfall.

🩹 Suggested fix
-    let max_rows = filtered.len().min(8) as u16;
-    let height = max_rows + 3; // border (2) + hint (1)
+    let content_rows = filtered.len().min(8).max(1) as u16;
+    let height = content_rows + 3; // border (2) + hint (1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let max_rows = filtered.len().min(8) as u16;
let height = max_rows + 3; // border (2) + hint (1)
let mut y = anchor.y + 1;
if y + height > bounds.y + bounds.height {
y = anchor.y.saturating_sub(height);
}
let popup = Rect {
x,
y,
width,
height: height.min(bounds.y + bounds.height - y),
};
f.render_widget(Clear, popup);
let title = if currencies.is_empty() {
format!(" Select currency ({} common) ", options.len())
} else {
format!(" Select currency ({} accepted) ", options.len())
};
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.style(Style::default().bg(BACKGROUND_COLOR).fg(PRIMARY_COLOR));
let inner = block.inner(popup);
f.render_widget(block, popup);
let split = Layout::new(
Direction::Vertical,
[Constraint::Min(1), Constraint::Length(1)],
)
.split(inner);
if filtered.is_empty() {
f.render_widget(
Paragraph::new(Span::styled(
" no match",
Style::default().fg(Color::DarkGray),
))
.style(Style::default().bg(BACKGROUND_COLOR)),
split[0],
);
} else {
let items: Vec<ListItem> = filtered
.iter()
.map(|o| {
let mut spans = vec![Span::styled(
format!("{:<5}", o.code),
Style::default().add_modifier(Modifier::BOLD),
)];
if !o.name.is_empty() {
spans.push(Span::styled(
o.name.clone(),
Style::default().add_modifier(Modifier::DIM),
));
}
ListItem::new(Line::from(spans))
})
.collect();
let list = List::new(items)
.style(Style::default().fg(Color::White).bg(BACKGROUND_COLOR))
.highlight_style(
Style::default()
.fg(Color::Black)
.bg(PRIMARY_COLOR)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("› ");
let mut state = ListState::default().with_selected(Some(selected));
f.render_stateful_widget(list, split[0], &mut state);
if filtered.len() > split[0].height as usize {
let mut sb_state = ScrollbarState::new(filtered.len()).position(selected);
f.render_stateful_widget(
Scrollbar::default().orientation(ScrollbarOrientation::VerticalRight),
split[0],
&mut sb_state,
);
}
}
let content_rows = filtered.len().min(8).max(1) as u16;
let height = content_rows + 3; // border (2) + hint (1)
let mut y = anchor.y + 1;
if y + height > bounds.y + bounds.height {
y = anchor.y.saturating_sub(height);
}
let popup = Rect {
x,
y,
width,
height: height.min(bounds.y + bounds.height - y),
};
f.render_widget(Clear, popup);
let title = if currencies.is_empty() {
format!(" Select currency ({} common) ", options.len())
} else {
format!(" Select currency ({} accepted) ", options.len())
};
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.style(Style::default().bg(BACKGROUND_COLOR).fg(PRIMARY_COLOR));
let inner = block.inner(popup);
f.render_widget(block, popup);
let split = Layout::new(
Direction::Vertical,
[Constraint::Min(1), Constraint::Length(1)],
)
.split(inner);
if filtered.is_empty() {
f.render_widget(
Paragraph::new(Span::styled(
" no match",
Style::default().fg(Color::DarkGray),
))
.style(Style::default().bg(BACKGROUND_COLOR)),
split[0],
);
} else {
let items: Vec<ListItem> = filtered
.iter()
.map(|o| {
let mut spans = vec![Span::styled(
format!("{:<5}", o.code),
Style::default().add_modifier(Modifier::BOLD),
)];
if !o.name.is_empty() {
spans.push(Span::styled(
o.name.clone(),
Style::default().add_modifier(Modifier::DIM),
));
}
ListItem::new(Line::from(spans))
})
.collect();
let list = List::new(items)
.style(Style::default().fg(Color::White).bg(BACKGROUND_COLOR))
.highlight_style(
Style::default()
.fg(Color::Black)
.bg(PRIMARY_COLOR)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("› ");
let mut state = ListState::default().with_selected(Some(selected));
f.render_stateful_widget(list, split[0], &mut state);
if filtered.len() > split[0].height as usize {
let mut sb_state = ScrollbarState::new(filtered.len()).position(selected);
f.render_stateful_widget(
Scrollbar::default().orientation(ScrollbarOrientation::VerticalRight),
split[0],
&mut sb_state,
);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/order_form.rs` around lines 651 - 732, The popup sizing in the
currency selection render path can leave the empty-state message with no space
when filtered is empty. Update the sizing logic in the render block around the
popup Rect/Layout split so the inner area always reserves enough rows for both
the "no match" text and the hint, and make the height calculation in the
currency popup rendering robust against the zero-item case. Use the existing
symbols filtered, max_rows, popup, and the [Constraint::Min(1),
Constraint::Length(1)] split to locate the fix.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, something went wrong! Please try again later. 🐰 💔

Show raw editable text for Amount, Premium, and Expiry while focused so
the caret aligns with visible input. Reserve minimum height for the
currency dropdown empty-filter state. Deduplicate blank/duplicate codes
in resolve_options. Add MD040 language tag to TUI_INTERFACE codegen block.

Co-authored-by: Cursor <cursoragent@cursor.com>
@mostronatorcoder

Copy link
Copy Markdown
Contributor

I did a pass on this PR and I do not currently see a blocker.

What I specifically checked:

  • the leave-guard flow around CreatingOrder -> ConfirmLeaveOrder -> adjacent tab
  • draft preservation / restoration through AppState.order_form_draft
  • the new currency picker state and filtering path
  • whether the UI redesign accidentally changes the existing order submission semantics

My read is that the behavioral changes are scoped in the right places.

The most important correctness point here was the leave guard: it needs to protect against accidental tab navigation without trapping the user or silently dropping state. The current flow looks sound to me:

  • dirty form + left/right navigation opens ConfirmLeaveOrder
  • Keep editing returns to the same form untouched
  • Leave stores the draft and switches tabs
  • returning to the Create New Order tab restores the saved draft
  • explicit Esc from the form discards the draft, which is also a sensible distinction from "leave and keep it"

I also checked the new currency picker path. The instance-advertised accepted list / bundled fallback split looks reasonable, and I do not see it changing the underlying order field semantics beyond replacing fragile free-text entry with a controlled selection flow.

I ran the local test suite on this branch as well, and it passed.

So from my side this looks like a solid UI/UX improvement pass without an obvious correctness regression in the order-creation flow.

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the current head. Overall this is a solid UX improvement.

One thing to watch: the currency picker now treats any alphanumeric key on the Currency row as picker input, so modified shortcuts like Ctrl+V are also swallowed there. If you want that row to stay shortcut-friendly, gate the picker-open path on unmodified typing only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant