Reported by a tester: the input field in v1.5.6 had right-click actions,
the current one has no right-click at all.
Correct, and the commit before this one put those actions in the wrong
place. The v1.5.6 menu was ImRaii.ContextPopupItem("ChatInputContext")
bound to the input field itself; I hung the two token entries off the
chat-message menu, where the rest of that window's items had landed.
They are on the input field now, which is also where they are useful:
you insert a token while composing, not while reading.
Two details from the original that the misplaced version had lost:
- The agent pointers are null-checked. Both can be null during a zone
transition, which is exactly when somebody is most likely to be typing
a flag into party chat.
- Inserting refocuses the field and puts the caret behind the token.
Picking from a menu and then having to click back into the field is
the kind of friction that makes a feature go unused.
Hiding the chat is not repeated in the menu. It was in the v1.5.6 one,
but it has its own button two widgets to the right now, and one way in
is enough.
Reported against v1.11.0: type a command, send it, need it again, and
up-arrow does nothing. v1.5.6 recalled it.
Everything needed was already in the tree. InputHistoryService holds the
last thirty entries with move-to-newest dedup, CompactInputHistoryNavigator
owns the cursor maths, and both have their own test mirrors. Neither had
a single caller, and the input field never set CallbackHistory, so ImGui
had no reason to raise the event in the first place.
So: the flag goes on, the callback grows a history branch, and TrySend
pushes the trimmed line before it clears the buffer. Editing ends the
walk, otherwise down-arrow after changing a recalled line would jump to
the next entry and discard the edit.
The cursor is per input bar while the history is global. Which line each
window is looking at is not something the others should inherit.
Last commit hid the header row when the title bar was on, to stop the tab name
appearing twice. The close button was in that row, and the title bar carries
none of its own -- ShowCloseButton is off because closing has to go through the
pool to release the slot. So a pop-out with its title bar on could not be closed
at all.
Pop-in moves into the input row, alongside the settings and hide buttons that
are already there. It is red, contrast-checked against the button plate it sits
on, and carries a tooltip, since a bare X next to an emoji picker does not say
where the tab is going.
Wired by setter after construction rather than through the constructor: the
window is what the button has to call, and it does not exist yet while its own
input row is being built. Routing it through the graph would close a
factory-callsite cycle MS.DI cannot detect.
The header row is now just the tab name, drawn only when there is no title bar
to carry it.
InputBar_PopIn_Tooltip ships in all 25 languages, including the Designer entry.
New strings get translated with the change from here on, not batched.
The three quick-button tooltips were hard-coded English while the rest of the
UI goes through HellionStrings. All 25 resource files now carry them, machine
translated on Flo's go-ahead.
Verified: every file still parses as XML, and the base file stays English so a
missing language falls back to what was there before.
Two things Flo hit on the first run.
The context menu threw on EndPopup. The spacing guard added in 929188e was a
`using var`, which disposes at the end of the method -- after EndPopup. ImGui
asserts when a popup closes with a style var still on its stack. The body moved
into a scoped block so the pop happens inside the popup.
The quick-button tooltips were empty boxes. SetTooltip ran inside the
FontAwesome push, and that atlas has no ASCII glyphs, so the text had nothing to
render with. ImRaii.DefaultFont did not save it. The hovered label is collected
now and drawn after the font is popped. Same trap as the unread badge in C5,
different place.
Those three tooltip strings are still hard-coded English while the rest of the
UI is localised. Pre-existing, and adding resources means touching 24 language
files, so it is noted rather than fixed here.
What blocks A to E did not already touch: the honorific header height and its
two offsets, the message list dummy widths, and the quick-button reserve in the
input bar.
The reserve is the one with visible consequences. At 150% the buttons grow with
the font while a fixed 130px column does not, so they stopped fitting.
The honorific offsets are centred rather than scaled. The 8f there was
(30 - 14) / 2 for the old font, structurally the same case as the sidebar: a
scaled constant keeps its mis-centering, a computed one does not.
The pill was already hand-drawn here -- filled rect, rounding 6, a frozen 3px
text offset and an InvisibleButton over the top. That is the Pill widget, built
before the widget existed, so it becomes the widget's first consumer. Its text
now centres against the measured line height instead of the frozen offset.
InputBar.Height moves to Metrics as well. MainWindow and ChannelPopoutWindow
both reserve their body height against it, so both follow without changes. It
had to happen in the same commit: the pill inside the bar now scales, and a bar
that did not would have clipped it at 150%.
The eye/hide button now hides the HellionChat window (runtime-only, via a new
DrawConditions gate) instead of toggling native-chat suppression, matching 1.5.6.
The chat-activation keybind (Enter / "/"), whose dispatch was a dead stub in the
KeybindManager since the v1.6.0 rewrite, is re-wired to MainWindow.ActivateChat:
it un-hides, opens if closed, brings the window to front and focuses the input --
so the chat reacts to Enter again from any state. /hellion is a reliable one-press
recovery (Toggle now clears the hide), and the window always shows on login
(start state no longer read from the persisted flag). Adds HideRestoreSelfTestStep.
Re-wires four 1.5.6 settings that survived the v1.6.0 rewrite as dormant config
fields but lost their UI + consumers:
- ShowTitleBar / ShowPopOutTitleBar: gate ImGuiWindowFlags.NoTitleBar on the main
window (ResolveFlags) and pop-out windows (new PreDraw). Inverted logic matches
1.5.6 (flag set only when the toggle is off).
- ShowHideButton: gate the input-bar hide button on the toggle.
- Use24HourClock: add the toggle (MessageList already consumes the field).
New 'Window style' section in WindowTab; Use24HourClock in ChatTab display modes.
MainWindowFlagsStep extended with the NoTitleBar fresh-base contract.
Brings back the 1.5.4 quick-picker lost in the v1.6.0 rewrite: a palette button
in the input-bar button row (left of the cog) opens a popup that switches the
theme (built-in + custom, active row checked) and jumps between chat tabs without
opening settings. Theme switch mirrors the settings ThemePicker; the tab jump
routes through a new MainWindow.ActivateTab that replays the click path
(previous -> set -> OnTabActivated) so tell/unread handling is unchanged. Main
window only -- pop-out InputBars get a null picker. Adds QuickPickerSelfTestStep.
Input-bar tells went out as a bare "/t" without the target, so the game
rejected them with "you must add the World name". Rebuild the full
"/tell name@world" from the 1.5.6 target chain in a pure BuildOutgoing:
- leg2/leg3 gated on current == Tell so a stale tell target on a Say tab
can't send a say line silently as /tell (CORR-1)
- world-resolve gate: an unresolvable world falls back to the channel
prefix, never "/tell name@ text" (COMP-1)
- ResetTempChannel after the send, tell-only
Also clear the runtime tell state on PromoteToPermanent so a promoted tab
can't route a typed line to the old partner, and surface the tell partner
("-> name@world") in the channel pill so a misfire stays visible. Adds
tell-routing and pill-transparency self tests.
v1.7.0 component-layer refactor removed ChatLogWindow.cs (which housed
the auto-translate popup) and dropped Ui/AutoCompleteInfo.cs without
migrating the logic into the new InputBar component — v2.x spec §3
said "LÖSCHEN + Logik migrieren", but only the deletion happened.
Result: Tab key did nothing in v1.7.1, and even manually typed
<at:group,key> tokens were never resolved into real auto-translate
payloads on send.
Root cause confirmed empirically via VN-1 diagnostic build
(_logger.LogDebug in SlashCommandCallback proved CallbackCompletion
fires on Tab once the flag is set). Following the diagnose-zuerst
pattern established by Issue #3 to avoid the source-code-only
hypothesis trap from Issue #2.
Migration follows v1.5.6 ChatLogWindow.DrawAutoComplete + ChatTwo
upstream AutoCompleteHandler patterns, but ported to v1.7.0 stil:
- ImGuiInputTextFlags extended with CallbackCompletion (Tab trigger)
and CallbackAlways (cursor restore via _activatePos analog v1.5.6
ActivatePos)
- SlashCommandCallback now dispatches three branches: CallbackAlways
(cursor restore), CallbackCompletion (Tab → word-boundary search
via Encoding.UTF8.GetString on the byte span, char-offset DTO
construction to avoid the byte-vs-char drift in v1.5.6's raw
pointer arithmetic), CallbackEdit (existing slash-command help
detection, now properly scoped)
- 7 new private state fields (_autoCompleteInfo, _autoCompleteOpen,
_autoCompleteList, _fixCursor, _autoCompleteSelection,
_autoCompleteShouldScroll, _activatePos)
- DrawAutoCompletePopup renders the picker at the end of Draw():
IsWindowAppearing seeds _fixCursor + focus, ListClipper-wrapper
from Util/SearchSelector.cs (IDisposable, automatic Destroy) for
the result list, Ctrl+0-9 quick-pick, Enter/Escape handling,
char-splice commit (_pendingMessage = before + replacement + after)
- AutoCompleteCallback handles popup-input-field fix-cursor seeding,
Up/Down navigation with wrap-around, Tab cycle in the default case
- TrySend now runs AutoTranslate.ReplaceWithPayload(ref bytes) and
sends via ChatBox.SendMessageUnsafe(byte[]) with a manual 500-byte
guard, because SendMessage(string) would route through SanitiseText
which destroys the binary SeString macro bytes that
ReplaceWithPayload emits
- AutoCompleteInfo DTO added as sealed internal companion type at the
end of InputBar.cs (15 LOC, exclusively consumed by InputBar);
ToComplete is a mutable field rather than auto-property so it can
be passed as ref to ImGui.InputTextWithHint without CS0206
Verified in-game (Flo): Tab on empty input opens picker with full
list, "fire" + Tab filters correctly, Up/Down/Tab navigate, Enter
commits <at:group,key>, send resolves to real auto-translate payload
in chat, Ctrl+0-9 quick-pick works, Escape closes without commit.
dotnet build clean, dotnet csharpier check clean.
Single minor plan-drift: scroll-to-selected uses
ImGui.SetScrollY(selection * lineHeight) instead of
SetScrollFromPosY(clipper.StartPosY) because the local
ListClipper-wrapper does not expose StartPosY — same UX effect.
InputPreview was only rendered for PreviewPosition.Top/Bottom (the
DrawConditions IsWindowMode gate). Inside-mode (the default) and
Tooltip-mode had no caller at all because v1.5.6's inline-render path
lived on the deleted ChatLogWindow and was not migrated to the v1.7.0
Components-Layer.
Wire Inside-mode by calling CalculatePreviewHeight + DrawPreview
inline from MainWindow.DrawMainArea between the message-list child
and the input bar, with the message-list height reserved for the
preview block. Wire Tooltip-mode by sampling IsItemHovered() on the
input text widget inside InputBar.DrawInputField (analog to the
existing _isFocused = ImGui.IsItemFocused() idiom on the same line)
and exposing it as WasInputTextHovered; MainWindow opens the tooltip
after _input.Draw when both the hover-flag and PreviewPosition.Tooltip
are active.
Plan-drift acknowledged: the plan stated Plugin.InputPreview is
statically reachable, but the property was declared as an instance
member on Plugin.cs:101. Hoisted to internal static to match the
plan's intention (analog to Plugin.Config); updated the single
external instance-access site in PluginLifecycle.RegisterWindows
to the type-qualified form.
Verified in-game: Inside-mode preview block appears between message
list and input bar on first keystroke; tooltip-mode shows preview on
text-field hover only; Top/Bottom-mode unchanged; empty buffer hides
the preview in all modes. dotnet build clean, dotnet csharpier check
clean.
J2 closes the trigger-gap discovered in J review (2026-05-27): J
migrated CommandHelpWindow as a window but the v1.5.6 trigger-path
was never ported. J2 restores it:
- InputBar.cs adds ImGuiInputTextFlags.CallbackEdit + character-level
callback that reads data.BufTextSpan, detects /-prefix, extracts
command word, and calls _commandHelpWindow.Value.UpdateContent(desc)
- AllCommands.cs (new file, 1:1 port from v1.5.6) populates a static
Dictionary<string, TextCommand> from Sheets.TextCommandSheet at
startup; Plugin.CommandManager.Commands is the fallback for
non-hardcoded commands
- CommandHelpWindow injected into InputBar via Lazy<T> ctor param to
break the InputBar <-> CommandHelpWindow circular dep; PluginHostFactory
DI-reg extended with the Lazy wrapper accordingly
Closes the smoke-step-9 gap. Phase-3 windows are now all reachable
end-to-end (R1 InputPreview, R2 CommandHelpWindow, R3 DebuggerWindow).
Replaces v1.5.6's direct LogWindow.Chat mutation pattern with typed mutators
that LogWarning + clip/drop on BufferCapacity overflow (silent-overwrite
semantics preserved, but overflow is now observable via /xllog).
Plumbing for v1.7.1 PayloadHandler resurrection — DrawPlayerPopup (tell-
prefix) and DrawStatusPopup (status-link append) will call these mutators
instead of mutating a public field.
Three smoke bugs from the second in-game test:
1. The channel pill was draw-list only, so it didn't react to clicks
and there was no way to switch channels inside a tab. The pill now
has a hit area on top and opens a popup that lists every ChatType
in tab.SelectedChannels with a ToInputChannel mapping; selecting
one writes through CurrentChannel.SetChannel.
2. Switching to Allgemein / Gruppe / Linkshell still showed "—"
because tab.CurrentChannel.Channel stayed at Invalid until somebody
set it. The sidebar now seeds CurrentChannel on tab activation by
walking SelectedChannels for the first key with a valid mapping,
so every tab opens with its own real channel instead of inheriting
the FC default.
3. MainWindow still surfaced an outer scrollbar next to the message
list's own scroll. Adding NoScrollbar + NoScrollWithMouse to the
window flags strips the second bar — the body child owns scroll on
its own.
Plus the system-icon path: System / BattleSystem / GatheringSystem /
Error / Notice / LootNotice all map to fa-cog now, so the System tab
renders the gear instead of falling back to the generic comment.
Five smoke bugs from the first in-game test:
1. Sidebar showed fa-comment for every tab because the resolve path
only honoured tab.Icon. Channel-type fallback restored — auto-tell
tabs render the envelope, the rest map their first SelectedChannels
key onto FontAwesome (Linkshells → link, FC → users, Party →
user-friends, System/Echo → cog, emotes → comments).
2. InputBar's channel pill read from tab.Channel (the saved default),
which is null on most non-FC tabs and rendered as "—". The pill now
reads tab.CurrentChannel.Channel first so the runtime input state
surfaces on every tab, with the saved default as a second fallback.
3. MessageList was making its own ImRaii.Child inside the main-area
child MainWindow already owns. That nested scroll created the second
scrollbar on the outer window. The component now lays out directly
into the parent's scroll region.
4. The input field reserved 90px for the three FontAwesome buttons,
which clipped them on standard frame padding. Reserve raised to
130px so the trailing buttons fully render.
5. Pressing Enter dropped the buffer — there was no send wiring. The
field now uses ImGuiInputTextFlags.EnterReturnsTrue and routes the
pending message through GameFunctions.ChatBox.SendMessage. Lines
that don't start with a slash get the active channel's prefix
prepended so typing in /fc lands on the FC channel instead of the
current game-side default.
InputBar gains an ILogger<InputBar> for the send-failure path; the
DI registration in PluginHostFactory is updated to match.
Top-level chat window composes HonorificHeader, Sidebar, MessageList,
InputBar and StatusBar in the layout from the master spec: header row,
horizontal body (sidebar + main area with messages + input), status
strip pinned to the bottom. Component types are fully qualified through
the Ui.Components prefix so the v1.5.6 Ui.StatusBar type cannot shadow
the new layer through parent-namespace resolution before it is removed.
Toggle is a new-shadow on Window.Toggle so the open path also writes
Config.MainWindowOpen; OnClose covers the close path through the base
behaviour. InputBar.Height is now public so the layout math can reach
it from outside the components folder.
Channel pill picks Token.AccentEmber when the tab is a tell (matched by
IsTempTab + a set TellTarget) and Token.AccentPrimary otherwise, so the
tinted background reads as the channel type at a glance. SymbolPicker
runs as an overlay popup — the inserted fragment splices straight into
the pending buffer up to a 500-char cap. Send wiring and the settings
button arrive when the main window assembles the components.