The v1.8.x sidebar/top-bar rebuild never re-rendered the unread dot, so inactive tabs showed no badge even though the counter was tracked. Draw it again top-right of the tab icon in both Sidebar and TopTabBar, gated on !active && UnreadMode != None && Unread > 0, and zero the active tab's counter every frame (1.5.6 convention) so the dot only ever shows on tabs you are not looking at.
The unread decision moves to MessageManager.ShouldCountUnread and snapshots the active tab + whether it shows the message once before the loop: Unseen suppresses unread on an inactive tab only when the active (real, post-F2) tab also shows that message. Adds SidebarUnreadDotStep (render) and UnreadDecisionStep (decision) self-tests (step count 32 -> 34).
Plugin.CurrentTab now delegates to MainWindow.ActiveTab (fallback Tabs[0]) instead of the never-assigned LastTab index, so the game hooks, unread tracking, notification sounds, InputDisabled and Foray/Eureka paths all operate on the tab the user actually has selected. The dead LastTab/WantedTab fields and both WantedTab writes are removed.
A reference-based MainWindow.ResetActiveTabIfRemoved repairs the active-tab reference on eviction/logout (immune to the SaveConfig temp-tab strip window). The worker-thread eviction path marshals it onto the framework thread so the strip mutation serializes with Draw; logout is already framework-thread. The Draw-seed gains a lazy re-seed for a wholesale config swap. Adds CurrentTabCouplingStep (headless) and the interactive CurrentTabGuidedStep self-test (step count 30 -> 32).
OnTabActivated clears the runtime tell state the game-side detour leaves on a tab (CurrentChannel tell target + partner label) when a DIFFERENT tab becomes the input surface, so a normal typed line can no longer route as a silent /tell to the old partner. Re-clicking the active tab and tabs carrying their own Tab.TellTarget binding (leg1) are preserved.
All four activation paths route through it: Sidebar, TopTabBar, ChannelPopoutPool.TryOpen, and the MainWindow draw-seed. EnsureCurrentChannel becomes a pure derive-helper reached only via OnTabActivated. Adds the TellResetOnActivateStep self-test (step count 29 -> 30).
Branch start for the LastTab-Decoupling fix cycle. csproj <Version> and repo.json AssemblyVersion + TestingAssemblyVersion go 1.8.5 -> 1.8.6. DownloadLinks and Changelog stay release-deferred on v1.5.6.
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.