Skip to content

fix(tui): line editor edge cases (merge on the last widget, move wrap, save guard return) - #689

Open
eric-engberg wants to merge 4 commits into
sirmalloc:mainfrom
eric-engberg:fix/line-editor-edge-cases
Open

eric-engberg wants to merge 4 commits into
sirmalloc:mainfrom
eric-engberg:fix/line-editor-edge-cases

Conversation

@eric-engberg

Copy link
Copy Markdown
Contributor

Three small line-editor fixes, one commit each.

What

  1. A merge left on the last widget can be cleared. On the last widget in a line, m now removes a merge that's still set there, and the help line offers (m)erge in that case. It still can't set a new merge on the last widget.
  2. A move that wraps keeps the other items in order. In the line editor's move mode and in the line selector's, moving an item past the top now puts it at the bottom (and past the bottom at the top) with everything else unchanged. Before, it swapped places with the item at the other end.
  3. The invalid-settings save guard returns to the current screen. With an invalid settings.json, Ctrl+S asks before overwriting the file. Whether you answer ESC or Yes, you now go back to the screen you pressed it on instead of the main menu.

Why

  1. Edit Line 1 → select Git Branch → m ((merged→)) → ↓ → d deletes Git Changes. Git Branch is now last and still shows (merged→). m does nothing there and (m)erge is gone from the help line. The next widget you add after it merges into it silently: add Git Changes back and the preview shows ⎇ main (+42,-10) in one segment, with no separator.
  2. Line 1 is Model, Context Length, Git Branch, Git Changes. Enter on Model (move mode), then ↑, gives Git Changes, Context Length, Git Branch, Model: Git Changes jumped from the bottom to the top. Lines do the same: with Line 1 (4 widgets), Line 2 (2 widgets) and Line 3 (empty), m on Line 1 then ↑ gives empty, 2 widgets, 4 widgets. The wrap-around came in with feat(tui): add wrap-around navigation to all menus and move modes #332 to move items "cyclically through list boundaries"; the swap is a side effect of reusing the neighbour swap for the wrap.
  3. With {"lines":"oops"} as settings.json: Edit Lines → Line 1 → Ctrl+S → ESC lands on the main menu, and you have to navigate back into the line. Yes does the same after saving.

How

  1. The m handler and the editor's canMerge now allow the last widget when it has a merge set. There, m clears the merge from either merged state. Everywhere else the merged → merged without padding → off cycle is unchanged. I picked this over stripping the merge automatically whenever a delete or move makes a widget last: that would silently drop a setting during a reorder (move a merged widget to the end and back and it would lose its merge), and it wouldn't help a merge already saved on a last widget.

  2. A small moveItem helper (src/utils/move-item.ts) removes the item and inserts it at the target. Both move modes use it. Moving to a neighbour is still a swap, so only the wrap changes.

  3. buildInvalidConfigSaveConfirm takes the screen to return to (it defaults to main). The Ctrl+S handler passes the current screen, and its Yes path goes back there before saving, so the success flash shows on that screen. Save & Exit starts from the main menu and still goes back there.

    Not fixed: the screen comes back the way it does when you navigate to it. A sub-editor that was open on it, such as the label editor with unsaved text, isn't restored. The confirm dialog replaces the whole screen, so ItemsEditor unmounts along with the sub-editor and draft it holds. Keeping them would mean showing the dialog over a screen that stays mounted, and pausing every screen's input handling while the dialog is up. That's a bigger change, so I left it out. The draft wasn't part of the save anyway, since a label is only applied on Enter.

Demo

Real TUI, recorded from main (before) and this branch (after) with the demo kit's sample config.

1. Merge on the last widget

Merge Git Branch, delete Git Changes, press m on Git Branch, then add Git Changes back.

Powerline: before

Merge left on the last widget can't be cleared, Powerline mode, before

Powerline: after

m clears the merge on the last widget, Powerline mode, after

Plain: before

Merge left on the last widget can't be cleared, plain mode, before

Plain: after

m clears the merge on the last widget, plain mode, after

2. Move past the top

Enter on Model, then ↑.

Powerline: before

Move wrap swaps Model with Git Changes, Powerline mode, before

Powerline: after

Move wrap takes Model to the bottom, Powerline mode, after

Plain: before

Move wrap swaps Model with Git Changes, plain mode, before

Plain: after

Move wrap takes Model to the bottom, plain mode, after

3. Save guard with an invalid settings.json

Edit Line 1 → Ctrl+S → ESC, then Ctrl+S → Yes. With an invalid settings.json the TUI shows its defaults whatever the file says, so there's no Powerline version of this one. The demo hides the parse warning the CLI prints to stderr at launch.

Before

Save guard returns to the main menu, before

After

Save guard returns to Edit Line 1, after

Testing

  • Tests that fail on main and pass here:
    • input-handlers.test.ts: m clears true and 'no-padding' on the last widget.
    • The two move-mode wrap tests now expect the other widgets to keep their order ([2, 3, 1] and [3, 1, 2]); main gives [3, 2, 1].
    • ItemsEditor.test.ts: (m)erge shows on a last widget that's merged, and m removes (merged→).
    • LineSelector.test.ts: the wrap past the bottom keeps the other lines in order.
    • App.test.ts: the guard builder honours the return screen. A whole-App test with an invalid settings.json returns to Edit Line 1 after ESC and after Yes, and checks the file was saved.
  • New tests that pass on both: the existing merge cycle, and that m can't set a merge on the last widget.
  • New and changed Ink tests passed 10 of 10 runs under Bun and under Node.
  • bun test: 2788 pass, 0 fail. bun run lint passes.
  • The four changed test files under Node (Vitest): 92 pass. main passes 86 in the same files (6 new tests).
  • Built CLI under Node 26.10.0 in tmux, with a scratch HOME and CLAUDE_CONFIG_DIR, ran all three flows above. A line-selector wrap with three different lines gave Line 2, empty, Line 1 on this branch and empty, Line 2, Line 1 on main. With the label editor open and "MyLabel" typed, Ctrl+S → ESC returns to Edit Line 1 (the label editor closes, as described above).
  • Bun 1.4.2, Node 26.10.0.

The merge toggle was guarded to widgets with another widget after them,
because the last one has nothing to merge into. But a widget can become
last with its merge still set: merge Git Branch into the widget after it,
then delete that widget. Git Branch still shows "(merged→)", m does
nothing, and "(m)erge" is gone from the help line, so the flag can't be
cleared. The next widget added after it then merges into it silently
(`⎇ mainModel: Claude` in the preview).

On the last widget, m now clears a merge that is set, and the help line
offers it there. It still can't set a merge on the last widget, and the
merged, merged-without-padding and off cycle is unchanged elsewhere.
Move mode wraps past either end of the list, but it did so by swapping the
moved item with the one at the other end. Moving Model up from the top of
Model, Context Length, Git Branch, Git Changes gave Git Changes, Context
Length, Git Branch, Model: Model reached the bottom, but Git Changes jumped
from the bottom to the top. The line selector's move mode did the same
with whole lines.

Both move modes now take the item out and put it back in at the target, so
wrapping past either end moves only that item and the rest keep their
order. A move to a neighbour is still a swap.
… guard

When settings.json is invalid, Ctrl+S first asks before overwriting it. The
question can come up on any screen, but both answers went to the main menu:
with Edit Line 1 open, Ctrl+S then ESC (or Yes) left the line editor, and
you had to navigate back in to carry on.

The guard now returns to the screen Ctrl+S was pressed on, for both
answers. Save & Exit, the other route into the guard, starts from the main
menu and still returns there. The screen comes back as it does when you
navigate to it: a sub-editor that was open on it, such as the label editor,
and its unsaved text aren't restored.
The App sets chalk's global color level from the settings it loads, and
the whole-App test helper didn't restore it. Test files that run later in
the same process then rendered at that level instead of their own: a
No Color check could see 256-color codes. The sandbox now restores the
level along with the mocks and the config path.
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