fix(chat): stop placeholder ghosting in chat composer (#65)

`TextEditor`'s NSTextView surfaces a typed glyph one frame before
the SwiftUI binding propagates, so the bare `if text.isEmpty`
overlay rendered the translucent placeholder text directly on top
of the just-typed character — the "behind or around" ghost the
reporter described.

Two mitigations:

- Pin an opaque `ScarfColor.backgroundSecondary` rect behind the
  placeholder Text. During any single-frame binding lag the user
  now sees a clean placeholder rather than layered glyphs.
- Switch the conditional to `.opacity(text.isEmpty ? 1 : 0)` so the
  view tree stays stable per keystroke. Pairs with the composer
  perf fix from #67.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-05-01 15:27:53 +02:00
parent df1b9caabf
commit fcfe1c89d6
@@ -108,16 +108,33 @@ struct RichChatInputBar: View {
)
)
.overlay(alignment: .topLeading) {
if text.isEmpty {
Text(supportsImagePrompts
? "Message Hermes… / for commands · drag images to attach"
: "Message Hermes… / for commands")
.scarfStyle(.body)
.foregroundStyle(ScarfColor.foregroundFaint)
.padding(.horizontal, 14)
.padding(.vertical, 10)
.allowsHitTesting(false)
}
// Placeholder ghosting (#65): TextEditor's
// NSTextView updates the visible glyphs a frame
// before the SwiftUI binding propagates, so a
// bare `if text.isEmpty` overlay renders the
// translucent placeholder text on top of the
// just-typed character visible as a "behind
// or around" ghost. Two mitigations:
//
// 1. Pin an opaque rectangle behind the
// placeholder text. During any single-
// frame lag the user sees a clean
// placeholder, never layered glyphs.
// 2. Use `.opacity(...)` instead of an `if`.
// Keeps the view tree stable per
// keystroke (removes the per-keystroke
// view-mutation churn the composer was
// already paying for).
Text(supportsImagePrompts
? "Message Hermes… / for commands · drag images to attach"
: "Message Hermes… / for commands")
.scarfStyle(.body)
.foregroundStyle(ScarfColor.foregroundFaint)
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(ScarfColor.backgroundSecondary)
.opacity(text.isEmpty ? 1 : 0)
.allowsHitTesting(false)
}
// Drag-drop image attachments. Receives both file URLs
// (from Finder) and raw image bitmap data (from