Skip to content

In-TUI editor for comments/replies (no more $EDITOR handoff for short text) - #7

Merged
jinyeow merged 10 commits into
mainfrom
feature/inline-editor
Jul 13, 2026
Merged

jinyeow merged 10 commits into
mainfrom
feature/inline-editor

Conversation

@jinyeow

@jinyeow jinyeow commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Types comments and replies in a fast in-TUI editor instead of handing the terminal to $EDITOR. Stacked on PR #4 (base feature/diff-view-enhancements); sibling to the theming PR (#6). Retarget to main after #4 merges.

Why

The $EDITOR suspend/resume handoff (ADR 0009) is the root of a bug class on Windows Terminal — neovim's slow terminal-capability start (E1568), driver quirks, blank screens — and can only be UAT'd, never unit-tested. For a one-line comment (the thread-id prompt literally launched an editor to type a number) that's absurd. lazygit-style hybrid: built-in widget for short text, $EDITOR for long-form + on demand.

What (ADR 0020)

  • New UI-free ITextInput.ReadAsync(TextInputRequest) → string? seam; TextInputDialog (editable TextView, or single-line TextField) + TuiTextInput. Headless-testable via NewKeyDownEvent.
  • Keys: Enter submits · Esc cancels · Ctrl-J (or Ctrl-Enter where the terminal delivers it) newline · Ctrl-E escalates to $EDITOR and returns for review.
  • In-TUI: line comments, thread replies, PR replies, PR-level comments; thread-id + assignee (single-line). Stays $EDITOR: descriptions, tags.

How it was built

Contract-lock → 2 parallel lanes (widget / migration) → integrate + wire → review-fix-loop (Codex + Fable). 691 tests, 0 warnings.

Review-fix-loop findings (all fixed)

Both reviewers independently cleared the deadlock risk (Ctrl-E suspends TG from inside a nested modal loop — decompiled proof it pumps correctly). Fixed:

  • HIGH: assign on an empty field no longer unassigns the item (empty submit = dismiss).
  • HIGH: a failed Ctrl-E $EDITOR launch now surfaces in the dialog hint (was a silent no-op).
  • MEDIUM: close/resolve is once-only; the editor call is cancelled on close so it can't open over the shell or write to a disposed field.

Notes / needs UAT

  • Newline chord: TG 2.4.16 can't distinguish Ctrl/Shift+Enter, so it's Ctrl-J (which Ctrl+Enter often aliases to); the Win32-driver path is a defensive guess — confirm in a real terminal.
  • Known limitation (ADR 0020): multi-line paste on the Win32 windows driver (no bracketed paste) may submit at the first newline — use Ctrl-E for that.
  • Verify the Ctrl-E suspend/resume round-trip in a real terminal.

Base automatically changed from feature/diff-view-enhancements to main July 9, 2026 04:15
jinyeow added 8 commits July 11, 2026 17:29
Headless-testable Dialog + editable field (multi-line TextView or single-line
TextField) implementing the ADR 0020 in-TUI text entry: Enter submits, Ctrl+J
inserts a newline (probed: the dotnet/ansi driver delivers Ctrl+J as
Enter|CtrlMask, not J|CtrlMask; Ctrl/Shift+Enter aren't distinctly representable
so Ctrl+J is the fallback), Esc cancels, Ctrl+E hands the buffer to $EDITOR and
returns to the field. Built without a run loop (mirrors TextDialog) and driven
headlessly via NewKeyDownEvent.
Marshals the TextInputDialog build + modal app.Run onto the UI thread via
app.Invoke and completes a RunContinuationsAsynchronously TCS, so background
flows can await short text entry. The resolve callback captures the value and
requests stop; Ctrl+E wires EditorService.EditAsync as the $EDITOR hatch. Also
drops an unused field from TextInputDialog.
Comment and assign now read via the ADR 0020 in-TUI text-entry seam
instead of the $EDITOR handoff. WorkItemActions gains an optional
ITextInput dependency, defaulting to EditorTextInput(editor) as a
behaviour-preserving compatibility shim for the not-yet-migrated
WorkItemDetailDialog call site. Tags and description stay on $EDITOR
(ADR 0020 scope).
Line comments, thread replies, PR replies, PR-level comments, and the
resolve/reactivate/reply thread-id prompt now read via the ADR 0020
in-TUI text-entry seam instead of the $EDITOR handoff. ThreadViewDialog,
DiffReviewDialog, and PrDetailDialog each take ITextInput in place of
EditorService (no other flow in these three needed $EDITOR directly).

CobaltShell wires a single EditorTextInput(editor) placeholder into all
three plus WorkItemActions — a behaviour-preserving stand-in until the
in-TUI TuiTextInput widget (built in parallel) is swapped in.
…lose

Review-fix-loop (Codex + Fable) on the in-TUI editor:
- HIGH: assign with an empty field no longer unassigns the item (empty submit
  is a dismiss, like the old $EDITOR no-op) — guard IsNullOrWhiteSpace.
- HIGH: a failed Ctrl-E $EDITOR launch now surfaces in the dialog hint instead
  of silently refocusing; the fire-and-forget task observes all exceptions.
- MEDIUM: close/resolve is once-only (a _closed guard) so queued keys can't
  double-submit and Ctrl-E post-backs can't touch the field after close; the
  editor call is cancelled on close so a queued suspend can't open over the shell.
- Doc: note the Win32-driver multi-line-paste-submits-early limitation (ADR 0020).
Rebasing onto main merged its RefreshBody feature (ThreadViewDialog now
subscribes vm.Changed → app.Invoke) into the editor branch's ITextInput
migration. Two headless-test consequences:

- The RefreshBody_* tests (main-side) passed the old EditorService via a
  NoopEditor() helper that no longer exists post-migration; switch them to the
  file's own FakeTextInput(null) no-op for the new ITextInput ctor.
- The reply success test can no longer assert the post synchronously:
  RunThreadMutationAsync fires Changed (→ app.Invoke) before the store call and
  this suite never Application.Init()s. Assert the request only, mirroring the
  identical documented pattern in DiffReviewDialogKeyTests; the posted-text
  behavior stays covered by WorkItemActionsTests.
@jinyeow
jinyeow force-pushed the feature/inline-editor branch from 76e7e39 to c847b69 Compare July 11, 2026 14:20
Comment thread tests/Cobalt.Tui.Tests/Screens/DetailDialogKeyDeliveryTests.cs Dismissed
/// <param name="ct">Flows into the Ctrl+E editor call.</param>
/// <param name="post">Marshals the Ctrl+E result back onto the UI thread; defaults to
/// <see cref="IApplication.Invoke(Action)"/>. Injectable so a test observes the refill synchronously.</param>
public TextInputDialog(
Comment thread src/Cobalt.Tui/Screens/TextInputDialog.cs Dismissed
Comment thread src/Cobalt.Tui/Screens/TextInputDialog.cs Dismissed
Comment thread src/Cobalt.Tui/Screens/TuiTextInput.cs Dismissed
…er key edges

Deep-review fixes on the in-TUI text field (ADR 0020):

- Ctrl+E in-flight guard (_editorOpen): the suspend/resume handoff is deferred
  via the UI-thread marshal, so a double-press in the pre-park window queued a
  second $EDITOR launch over the same buffer (last-write-wins clobber). Guard
  blocks concurrent re-entry only; sequential Ctrl+E->edit->return->Ctrl+E still
  escalates. Reproduced RED (two launches) then GREEN in the headless harness.
- Collapse the two byte-identical newline-chord branches into one guard.
- Characterization tests: empty-Enter submits "" (not null), Shift+Enter newline
  arm, single-line Ctrl+Enter submit / Ctrl+J no-op guard branch.
Comment on lines +174 to +176
if (!_request.SingleLine &&
((baseCode == KeyCode.Enter && (key.IsCtrl || key.IsShift)) ||
(baseCode == KeyCode.J && key.IsCtrl)))
Clears CodeQL cs/xmldoc/missing-summary (#44, #45); both ctors had full
<param> docs but no summary, inconsistent with the repo docstring convention.
@jinyeow
jinyeow merged commit 29a4b95 into main Jul 13, 2026
5 checks passed
@jinyeow
jinyeow deleted the feature/inline-editor branch July 13, 2026 10:38
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.

2 participants