Imported from afollestad/alveary (
Alveary/Views/Components/TextInput/AGENTS.md). Install upstream withnpx skills add afollestad/alveary --skill TextInput. Copyright stays with the author.
Text Inputs
Rules for AppMarkdownEditor, AppTextEditor, AppKitTextView, and their companions.
AppMarkdownEditor
AppMarkdownEditor + AppMarkdownDraft are the app's shared BlockInputKit host. This section owns their rules; Alveary/Views/PullRequests/, Alveary/Views/Settings/, Alveary/Views/Skills/, Alveary/Views/Scheduled/, and Alveary/Views/DiffViewer/ consume them.
- Reach for it whenever the content is markdown prose — comments, agent prompts, commit messages, skill and scheduled-task instructions. Keep
AppTextEditorfor raw non-markdown text a block editor would reformat, such asMCPServerPane's headers and env blocks, andAppTextFieldfor single-line values. - Never write serialized markdown back into a
Stringwhile the user types.BlockInputView.configurealways reloads the document from its store, so a per-keystroke write-back reconfigures the editor mid-edit. Hosts hold the draft and readdraft.markdownat their own commit point — Save, Submit, andonDisappearwhen a closed pane must reopen with its text. - Swap a whole document with
resetContent(to:), splice withreplaceText(_:).resetContentbumpscontentGeneration, which keys the editor and rebuilds it — a caret left from the old document otherwise scrolls the new one under its top inset and clips the first block against the chrome. Reset, disk reloads, and generated bodies reset; mid-edit rewrites that must keep the caret (PR attachment links, composer text sync) usereplaceText. - Publish coarse state only on transitions.
isEffectivelyEmptyandmatchesReferencere-render the host, which reconfigures the editor; the guards inAppMarkdownDraftare what keep that to once per transition rather than once per keystroke. A new flag follows the same shape. - Size by line count, not pixels.
.growsToLineCountgrows with content then scrolls;.fillsAvailableHeighttakes the host's frame, for a resizable sheet. AScrollViewhost needs neither — put the editor in a sheet instead, rather than reintroducing a fixed-height mode to survive the unbounded vertical proposal. - Text put into a draft has to come back out unchanged.
AppMarkdownDraftTestscovers commit-message and instruction shapes;PromptDefaultsMarkdownRoundTripTestscovers the packaged prompts, whose authoring rules live inAlveary/Services/Settings/AGENTS.md.
AppTextField
showsClearButtondrawsAppTextField's own trailing clear affordance and is off by default; it owns the binding, so callers pass a flag rather than supplying a button. While visible it takes over the trailing inset so scrolled text and the caret cannot run underneath it, and clearing keeps focus in the field the wayNSSearchFielddoes.
AppKit Bridge
- Draw the placeholder inside
AppKitTextView, not as a SwiftUI overlay, so insets and caret placement match the real text view. - Keep the empty editor's caret and placeholder text on the same x-origin. Do not add a focused-only placeholder offset; it makes the caret appear left of the placeholder instead of lined up with the first typed character.
- Keep
AppKitTextView.allowsVibrancy = false.- Vibrancy can shift AppKit-drawn chip fills away from the literal
NSColorused by matching SwiftUI accent surfaces. - Disabling vibrancy keeps editor chips stable across composer panels, sheets, popovers, and future host surfaces.
- Vibrancy can shift AppKit-drawn chip fills away from the literal
- Selection-change callbacks must not synchronously trigger layout-dependent restyling.
- Lightweight typing state may update inline.
- Full chip/code restyles should defer to the next main-runloop turn.
- Prime text-container width with
updateTextContainerForCurrentBounds()from layout or measurement, not fromdraw(_:).draw(_:)and chip/hint rect helpers may callprepareForSafeTextLayout()as a read-only guard.- Use
markTextLayoutNeedsPriming()after text/attribute changes andprimeTextLayoutForDrawing()from measurement/layout before allowingNSTextView.draw(_:)to fill layout holes. - AppKit can draw during mount or SwiftUI update cycles while the text container still has a zero width.
- Mutating the text container or forcing
NSLayoutManagerglyph layout in that state has caused crashes inNSTextView.draw(_:)and height measurement.
sizesToContenteditors must handle binding-driven text replacement before AppKit has a stable layout width.- Prime SwiftUI height from explicit line breaks for immediate growth.
- Let the AppKit measured height refine the value after layout catches up.
- Keep delayed measurement guarded by the text value that was measured so stale async work cannot resize a newer draft.
- Use
showsDisabledCursoronly for disabled editors that should show a blocked cursor; normal progress-only read-only editors should leave it false. - Command-key equivalents can arrive through
performKeyEquivalent(with:)instead oftextView(_:doCommandBy:); keepAppKitTextView.onKeyEquivalentforwarding intoAppKitTextEditorCoordinator.handleKeyEquivalent(_:)for shared text-input callers that opt into key handling. - Cache derived text-presentation outputs across identical SwiftUI updates; width-only layout may restyle chip geometry, but plain text should only recalculate internal AppKit height, and fixed-height editors must not publish unused measured-height state.
Focus
focus: FocusState<Bool>.Binding?is an AppKit-to-SwiftUI bridge.- Programmatic focus must use
requestFirstResponder: UUID?plusonFocusRequestConsumed, not direct writes to@FocusState. handleFocusChangebackfills both the focus binding and the plainisAppKitFirstRespondermirror.- Body-time reads of first-responder state must use
isAppKitFirstResponder, not@FocusState. syncFocusIfNeeded()claims but never force-resigns — see the owning bullet inAlveary/Views/AGENTS.md's Focus And Keyboard Coordination.claimFirstResponder(on:retriesRemaining:)polls short main-runloop ticks until the text view has a window.- Do not replace the retry with one long sleep.
- Keep
firstResponderClaimInFlightdeduping around the retry chain. - Keep that flag's writes inside the main-queue body and do not clear it between recursive retry hops.
Chip Styling
- Keep base
textColorand typing color pinned to normal label color. Styled chip colors must not bleed into later plain text. applyTrailingKernis only for.slashCommandchips. File mentions and inline code sit mid-line and should not gain asymmetric trailing room.- Compact file-mention chips:
- Hide the entire stored encoded range with clear foreground.
- Use computed negative
.kernso the enclosing rect shrinks to the decoded label width. - Draw
CanonicalPath.decodeStoredMentionPath(chip.displayText)aftersuper.draw(_:). - Only draw compact labels for single-line chip rects.
Drops And Paste
disablesAppKitDragDestinationis opt-in per editor.- Set it to
trueonly when a parent.dropDestination(for: URL.self)handles drops. - Override
updateDragTypeRegistration()so NSTextView cannot re-register drag types after state changes. - Unregister all drag types when opting in; Finder also provides paths as
.string. - Do not override
readablePasteboardTypes; that breaks paste.