promotional bannermobile promotional banner

Fast Guild Invite - Revived

A WoW Classic AddOn to help recruit new members into your guild.
Back to Files

FastGuildInvite-v2.14.13

File nameFastGuildInvite-FastGuildInvite-v2.14.13.zip
Uploader
PmptastyPmptasty
Uploaded
Oct 4, 2026
Downloads
887
Size
7.3 MB
Flavors
RetailMoP ClassicClassic TBCClassicForever
File ID
9060987
Type
R
Release
Supported game versions
  • 12.1.0
  • 12.0.7
  • 12.0.5
  • 5.5.4
  • 4.4.2
  • 3.4.5
  • 2.5.6
  • 1.60.1
  • 1.15.9

What's new

<FGI> FastGuildInvite

[v2.14.13] (2026-10-04) — saved templates show again, the baton can be handed on, policy pushes dated by the server clock, and Cataclysm races

Fixed — saved templates had no rows until a new one was saved (FGI_Core.lua)

Player report (Discord, 2026-10-02): "the Your Templates section only shows up after saving a new template, even if you already have saved ones." GUI/TemplatesPage.lua builds one row per template at file load, and at file load SettingsProfile:Library() is still its empty stand-in because the DB is not bound yet. The only rebuild was OnLibraryChanged, which fires on a Store or Forget, so templates from an earlier session had no Apply, Forget or Send until the player saved another. OnInitialize now calls TemplatesPage:Refresh() straight after the login template is applied. zz_templates_page_spec pins the call's position after addon.DB = DB, and that a template already in the library gets a row on refresh. Not yet seen in game.

Fixed — the holder could not hand a role to anyone when the guild has no list for it (Modules/FGI_Recruiter.lua)

Player report: "FGI is not letting me assign the designated welcomer status to someone else ... However, he was able to use it to take it from me." Recruiter:CandidatesFor returned nobody when a role had no policy list, while MayHold (behind Take) falls open, so anyone could take the role and the holder had no Assign menu. That contradicted the file's own rule that a guild with no policy can pass the baton around freely. With no list it now offers every online guildmate who passes MayHold, sorted, leaving yourself out. recruiter_spec pins it.

Not confirmed as this player's cause. The welcomer list inherits the announcer list when unset, and the same player pushes policy lists, so their guild may well have one. For that case:

Changed — a refused Take or Assign now says why (GUI/Tabs/GuildRoster.lua)

Both menu clicks discarded AssignBaton's refusal, so a refused assign did nothing and said nothing. GuildRosterTab.ReportBatonRefusal prints one chat line naming the role and the reason (not on the list, or cannot take duty right now). AssignBaton returns a third value, "assigner", when the refusal is about the person assigning, so the line does not blame the target. Four new enUS keys (batonRefused*), client locale through fn.chatLoc. The next report from this player should name the gate that refuses them.

Fixed — names dropped out of the policy lists after Push policy to guild (Modules/FGI_DeltaSync.lua, GUI/SettingsPanel.lua)

Player report, WoW Forever (Discord, 2026-10-04, with before/after screenshots): after Push policy to guild, the first name ("Vishi Swaz", the pusher) was gone from the announcer, inviter and welcomer boxes, leaving "Sienara Drakos" in all three. The cause is not confirmed, and two changes ship for the two explanations the screenshots allow:

  • The lists really changed: a save was dated by the player's own PC clock. StampPolicySave stamped setAt with time(), and every leaf canon is ordered by that number on every client (canonBeats). An officer whose PC ran fast therefore won every leaf against any later push, and the next exchange put their older lists back. Reading the code found no other writer that drops one entry: the only production writers of the three lists are the boxes' Accept (parsePolicyNames resolves and de-dupes, never drops), the Clear buttons, and the merge in ApplyPolicy; our own echoed push is refused as identical (ApplyPolicy, the equal-hash return). It is now stamped with GetServerTime(), falling back to time() where that is absent. policy_sync_spec steers both clocks and pins a save stamped by the server while the PC clock is an hour slow. Clients still on an older version stamp with their PC clock until they update.
  • Only the drawing was wrong. AceConfigDialog rebuilds the page from inside the button's own click (ActivateControl, the full-refresh branch). The button now asks for one more redraw after its click has returned. zz_settings_panel_spec pins one redraw after the click and none inside it; its push examples now drain that redraw, which otherwise fired inside the Blizzard-panel example's clock and failed its re-feed count.

An earlier draft of this entry said the names came back once the tab was reopened. Nothing the player posted says that; the claim was mine and unsourced, and it is withdrawn. Neither change is verified in game.

Fixed — Cataclysm Classic had no Worgen or Goblin, and Wrath's race/class pairs (functions.lua, Locale/summary.lua)

fn.raceClassBranch mapped isCata to the wrath table (TBC plus Death Knight), and Locale/summary.lua gave Cata the TBC race list. So a Cata scan never searched for Worgen or Goblin, and never for Cata's new pairs (Human Hunter, Orc Mage, Dwarf Shaman, Tauren Paladin, Gnome Priest and the rest). A new cata table is generated the same way as the others, from CharBaseInfo joined to ChrRaces and ChrClasses for wow_classic build 4.4.2.60895: 12 races, 91 rows, every row accounted for. summary.lua gains a Cata race list with Goblin (9) and Worgen (22). The README and CurseForge caveats about Cata are replaced. Found on the way: wago.tools ignores build= when product= is also passed and serves the product's current build (Mists, with Monk and Pandaren); the functions.lua header now says so. Not changed: Cata still reads MoP Classic's instance list (fn.instanceAreaFlavour).

zz_raceclass_branches_spec.lua:46 now expects raceClassBranch({ isCata = true }) == "cata" (it asserted the defect, "wrath"). New examples pin the cata table (12 races, 91 pairs, Goblin, Worgen, no Monk) and add cata to the branch list.

Still open from the same reports

  • Templates not sending on WoW Forever. The reporter (2026-10-03): the sender's chat showed nothing, and the receiver got nothing. Every path past Wire:Send prints a verdict (AceCommQueue reports a refusal as delivered == false, ChatThrottleLib calls back on every non-throttle result) and a refusal to try prints its own line, so total silence points at the send never starting -- most likely the Send to box, which AceConfig commits only on Enter or its Okay button. Not confirmed. GUI/TemplatesPage.lua now prints "Sending the template X to Y..." the moment a send is handed over (new enUS key; two examples in zz_templates_page_spec), so the next report separates "never sent" from "lost in the queue".
  • Whispers to players already in a guild (crimsonmane, 2026-10-02). A full audit of the /who library and the queue (2026-10-03) found the scan-time guild gate sound, and the leak AFTER it: a stranger's guild is read once, from the /who that queued them, and never again. fn:queueEntryStale re-checks only our own guild and the anti-spam stores; invitePlayer drains the OLDEST row first (index or 1), so a deep queue always contacts the stalest candidate; and invite types 2 and 3 send the whisper before any server answer can exist. Only rows restored from a previous session are held for a fresh /who. That gap is a reading of the code, not a cause seen in the field: the operator's /fgi debug scan on WoW Forever (2026-10-03) rejected all 43 guilded players and queued 7 with an empty guild, and a hand-typed /who of one of them confirmed no guild in the game's own Who list (which shows guilds, checked against a <Nameless> member). No change made for it; the next evidence needed is a real whisper to a guilded player captured with /fgi debug on. The two defects below were found on the way and are fixed.

Fixed — "already in a guild" lost when the invite and the reply spelled the name differently (functions.lua, Modules/Scan.lua)

The field screenshot behind the guilded-whisper reports: "You have invited Teksuo-Myrzael to join your guild." then "Teksuo is already in a guild." The handler looked the reply up in pendingInvites by raw key, and off retail normalizePlayerName keeps both spellings as written, so the invite was never found. The player was not anti-spammed, the Type 4 decline whisper stayed queued under the qualified name, and the next guildless sighting offered them again. The decline branch had the same lookup. New fn.pendingKeyFor resolves the reply: exact key, then fn.playerKeyed's own-realm cases, then a bare reply against realm-qualified keys when exactly one matches (two in flight is refused, not guessed). promotePendingToAntiSpam, clearPending and the decline branch use it, and the auto-decline and in-guild branches clear msgQueue under the queued spelling as well. Five examples in zz_scan_system_handler_spec. Classic family only; retail and Forever normalize both sides alike.

Fixed — /who: retail and Forever read a missing guild as "no guild"; Classic lost rows after a gap (Libs/LibWho)

NormalizeWhoInfo_Retail wrote info.fullGuildName or info.Guild or "", so a row with no guild field passed the scan's p.Guild ~= "" gate as guildless. The Classic path keeps nil, which the gate drops. The or "" is gone from Guild and fullGuildName; pinned in zzzz_libwho_retail_timeout_spec. The client documents fullGuildName as never nil, so this is a fail-closed guard, not this report's cause. The Classic reader stored result[i] = info after skipping a nil row, leaving a hole that for i = 1, #results could stop at or index past into addNewPlayer(nil), which raised and lost the rest of the answer. It appends now, as the retail reader already did. No spec drives a nil row.

Tests — harness pinned at 09a0a8c (Tests/support/roster.lua, Tests/recruiter_spec.lua)

The WoWAPITesting pin moved from f6fd3d4 to 09a0a8c. Mainline is Interface 120100 there, so recruiter_spec expects that. Harness 349b7c3 moved GetNormalizedRealmName onto the player's realm (wow.realmName), and roster.setRealm only wrote guild.realm, which turned 41 connected-realm specs red; it now writes both and roster.reset puts the player's realm back. Two env gaps found on the way (STANDARD_TEXT_FONT, and UIDropDownMenuTemplate's 40x32 size) were staged here, delivered by the harness as 6767dae, and the stand-ins deleted the same day.

[v2.14.12] (2026-10-01) — WoW Forever is recognised again, raid markers on Forever, and the blacklist reason on the kick prompt

New — the "blacklisted player is in your guild" prompt shows the reason (functions.lua)

Player request (Discord, 2026-10-01), against a screenshot of the prompt: "This screen should show the blacklist reason". showNextKickPrompt now looks the entry up with IsInBlackList(name, "exact") at prompt time and adds a Reason: <text> line under the name. It reads both shapes the store holds, { reason, time, blacklister } tables and the bare reason strings older saves carry, the same way GUI/Tabs/Blacklist.lua does. An empty reason adds no line. It reuses the existing Reason locale key, so no locale file changed. zzzz_blacklist_kick_popup_spec pins all three cases. Not yet seen in game, including how the dialog's height takes the extra line.

Fixed — FGI never recognised WoW Forever; it ran as Classic Era there (FGI_Compatibility.lua)

v2.14.0 detected Forever as WOW_PROJECT_ID == WOW_PROJECT_MAINLINE plus an interface number below 100000. The Forever client reports project id 18. Measured in-client 2026-10-01 on build 1.60.1.70124 with /run FGI.gameVersion:PrintInfo(): Name: Classic (Fallback), Build: 16001, Project ID: 18, Identified: false (guessed), Is Retail: false, Is Forever: false, Is Classic: true. So from v2.14.0 to v2.14.11, every gv.isRetail and gv.isForever branch was dead on Forever, and it ran Classic Era's code: the class, race and zone lists, the retail API paths, the unit-menu combat and instance gates, and every Forever-specific fix shipped in v2.14.7 to v2.14.11. Forever's Blizzard_ProjectConstants/ProjectConstants.lua defines only 1 and 2, so there is no named constant. gv.isForever is now WOW_PROJECT_ID == 18, and gv.isRetail is true on Forever as the design always intended. zz_version_detect_spec drove Forever as MAINLINE, the same wrong assumption, which is why it stayed green. It now drives 18 and pins the pasted output.

Known cost: every retail branch now runs on Forever for the first time. Those branches were written and tested against live retail, not Forever. A Forever problem with any of them would be new in this version.

Not changed, and also checks for MAINLINE: the LibDBIcon-1.0 copy bundled only in the _Camelot TOC (Libs/LibDBIcon-1.0/LibDBIcon-1.0.lua:537, :576, :611), so it takes its non-retail path on Forever. It is that library's code; left as is.

Fixed — ADDON_ACTION_FORBIDDEN SetRaidTarget() from a unit-frame menu on WoW Forever (Modules/FGI_ChatMenu.lua)

Field report on v2.14.11: picking a raid marker from a right-clicked portrait was blocked and blamed on FGI. The stack ends in TargetFrame.lua:698, the line in the Forever tree (live's is 692). The player's observation: open world, out of combat, on v2.14.11. Because of the detection defect above, none of the retail menu gates (combat, instance) ran on Forever, so FGI added its entry to every unit-frame menu there. The generator also now adds nothing to the seven marker-hosting unit-frame menus on Forever, in or out of an instance (/fgi menulog says "unit-frame menu on WoW Forever"). Whether retail's instance-only rule alone would be enough on Forever is untested. This skip is the safer setting until it is. The chat, friends, Battle.net friend and guild menus are unchanged. Known cost: no FGI entry when right-clicking a target, focus, party or raid frame on Forever. zz_chat_menu_spec pins it. In-game, 2026-10-01, Forever, open world, solo: the detection printed WoW Forever / Is Forever: true; /fgi menulog showed FGI adding nothing to MENU_UNIT_PLAYER and MENU_UNIT_TARGET; setting raid markers from those menus raised no Lua error. With the skip turned off (/run FGI.gameVersion.isForever=false, retail's gates still active), /fgi menulog showed "FGI submenu added" on MENU_UNIT_TARGET once and MENU_UNIT_PLAYER three times, and setting raid markers raised no Lua error either. So the error did not reproduce with FGI's entry present on this build. The reporter was on v2.14.11, where the whole addon ran Classic Era code on Forever; whether the error came from the entry, from that, or is intermittent is not known. The skip stays as the safer setting.

[v2.14.11] (2026-09-30) — Hold warnings off by default, announcement fixes, and whisper tabs that wait for a reply

Fixed — wording that overstated what WoW Forever blocks

v2.14.9 and v2.14.10 said Forever "never lets" / "does not let" a welcome go out by itself. The evidence is one player's BugSack report of ADDON_ACTION_BLOCKED on the timer-driven welcome send. The CurseForge notes for both versions and the Forever note in the "Send welcomes automatically" tooltip now say Forever blocked FGI's automatic welcome, and the v2.14.10 heading below is reworded the same way. Behaviour is unchanged.

New — "Show recruiting hold warnings in chat"

Player report (Discord, 2026-09-29): the queueHeldNotice line read as spam on every press. Operator, 2026-09-30: "have a setting to hide these types of warning messages, so the users don't see them. have it set so they don't see them by default." New DB.global.showHoldWarnings (default false, Settings → General → Scanning, order 9, catalogued under notify in FGI_SettingsProfile). fn.showHoldWarnings() gates the four chat notices that explain a hold: queueHeldNotice, queueDroppedNotice, protectedPauseNotice and inviterHeldNotice. Only the chat line is gated; every hold still happens and the Scan tab's held count is unchanged. Recruitment-goal notices are not included -- they go through RecruitGoals:Notice, which also raises the goal prompt.

Known cost: with the default, a player whose invites are held gets no explanation in chat. That is what was asked for.

The kept-queue setting (persistQueue) was already false in the shipped defaults, so a new install has it off; a spec now pins both defaults. Existing players who switched it on keep their choice.

Specs: zz_invite_holds_spec (the notice examples switch the setting on; a new block pins the default holding silently), zz_invite_spec, zz_settings_panel_spec.

Fixed — an announcement that did not go out still started its cooldown

Player report (Vishiswaz, Forever, 2026-09-30): "the announcement did not go out / the timer happened / and I was definitely in guild chat". Announce:Send stamps the profile's cooldown and the rotation's timer and turn on the click that sends; the echo sweep reported the missing echo but never undid the stamps. The pending record now carries what the stamps overwrote, and sweepConfirms restores them via rollbackStamps, each only if it still holds our stamp, so a later post or a guildmate's sync is left alone. The next click retries the post.

Known cost: a guild announcement's cooldown push to peers (announceSync:PushPost) is not recalled, so guildmates keep that cooldown. And if a post really did go out but its echo was not recognised, the retry posts it again.

announceMsgDidNotPost no longer says "check you're in that channel", which made no sense for Guild; it now says the post never appeared in chat and the next click tries again. Why this post was dropped on Forever is not known.

Specs in announce_channels_spec: rollback of a profile, rollback of a rotation's timer and turn, an echoed post keeping its stamp, and a re-stamp surviving the rollback.

Fixed — the login hold on announcements could run out behind the loading screen

Vishiswaz confirmed the failed post came straight after a login. The 15 s LOGIN_ANNOUNCE_GRACE was armed at PLAYER_LOGIN / PLAYER_ENTERING_WORLD, and both fire while the loading screen is still up, so a slow load spent the window before the player could move; Wingman's first click then posted into a guild chat the server had not joined us to. The session's first LOADING_SCREEN_DISABLED (present in the Classic Era and Forever API trees) now restarts the window (Announce:LoadingScreenDone); later loading screens do not. Wingman already passes a held click on to scan or invite (tryAnnounce only consumes a click that posted). 15 s is unchanged; whether it is long enough on Forever after the load is not verified. Spec in announce_channels_spec.

Announcements also hold while the chat API reports its server down: CHAT_SERVER_DISCONNECTED sets the hold and CHAT_SERVER_RECONNECTED clears it (both events are in every flavour's ChatInfoDocumentation). Nothing is stamped while held. A normal Classic Era login fired neither (observed in-game 2026-09-30), so this covers a real outage, not the login window. Spec in announce_channels_spec.

Changed — a whisper tab opens when the player answers, not on every whisper sent

A player on Discord (rocky, 2026-09-30): "could it only spawn a whisper window when someone responds instead of on every outgoing text?" The operator asked for it. WT:OpenKeys (the tabs) now lists only conversations with replied set; the new WT:CapturedKeys lists every captured one, answered or not. An unanswered conversation is still recorded from its first line and still kept out of the game's chat, so the tab that appears on the reply already holds what was sent. The whisperMode hold now follows CapturedKeys, not the tabs, so the game still opens no window of its own for an outgoing recruiting whisper that has no tab yet. zz_whisper_tabs_spec updated to the new rule.

Fixed (unverified) — Guild Invite from the whisper tab's right-click menu was blocked

BugSack, 2026-09-30: ADDON_ACTION_BLOCKED on Invite() from FGI_WhisperTabs.lua run, called from the menu row's OnClick, through ChatMenu.GuildInvite -> API.GuildInvite -> C_GuildInfo.Invite. The call is inside a click, as the tray's own invite buttons are, and those go through. The one difference found is that the row hid the menu before running the action. The action now runs first and the menu hides after it. That the Hide is what cost the click its hardware event is not verified; it needs an in-game test on the client that raised it.

Changed — functions.lua comments trimmed

At 517 KB it was over the Lua language server's 500 KB preload limit, so the file went unscanned. The change-history narration in its comments was removed (257 KB, 5,947 lines). Checked by compiling it and the previous version, with debug info stripped, and diffing the listings: the instructions and constants are identical apart from this release's showHoldWarnings code.

[v2.14.10] (2026-09-29) — Wingman no longer stuck on a kept queue, and WoW Forever name fixes

Fixed — Wingman stuck on a queue kept from a previous session

Player report (Vishiswaz, WoW Forever, 2026-09-29): "wingman won't run because of this ... It doesn't tell me HOW to run a scan to re-confirm them". Every queued row was HELD (restored by persistQueue, guild not re-confirmed) and invite was first in the step order. tryInvite claimed the click whenever the queue was non-empty, fn:invitePlayer could contact nobody and printed the held notice, and the scan step -- the only thing that lifts a hold, via fn.releaseQueueHold in fn:addNewPlayer -- never ran. The invite step now passes the click on when #list <= fn.queueHeldCount(). Not Forever-specific: any flavour with a fully held restored queue hit it. queueHeldNotice now names the remedy (the >> scan button or Wingman, and Decline to remove one by hand). Pinned in zz_wingman_session_spec, run red first.

Fixed — WoW Forever: "Send welcomes automatically" showed ticked for a behaviour FGI does not use there

Peer Review, thread a08ac06a, finding 2. Since v2.14.9 Welcome:AutoSend() is always false on Forever, but the Settings box still read ticked. On Forever it is now shown unticked and disabled, and its tooltip says welcomes there always wait for a click. Live retail keeps auto-send; that it works there is untested -- no block has been reported, which is not the same thing.

Fixed — WoW Forever: another officer's first-name policy stamp could read as ours

Peer Review, thread a08ac06a, finding 1. policyIsOurs (GUI/Tabs/Filters.lua) accepted UnitName's first return as a match for the legacy setBy stamp. On Forever names are unique only as "First Last", so a policy stamped "Vishi" by another officer read as ours, and that answer decides whose guild policy we may overwrite. On Forever only the full name matches now; every other client is unchanged.

Both pinned in zz_forever_names_spec, each run red by reverting its fix line in place and restored.

The check now fails closed (Peer Review reply 3): the Forever flavour is asked as well as the client's RegionalUniqueNamesEnabled switch, in policyIsOurs and in fn.myName, which also joins the surname itself when LibGuildRoster's UnitKeyName hands back the first name alone. Spec run red first.

Fixed — WoW Forever: the By officer list showed one officer twice

Peer Review, thread a08ac06a, finding 3. Before v2.14.9 an officer's stats row was keyed by the first name alone; from v2.14.9 by the full name, so both rows were held and synced. At login (MemberStats:onLogin, after the own-row refresh) a first-name row is dropped once a full-name row with that first name is held; acceptRemote refuses such a row from a peer still on an older copy, and a full-name row arriving retires the first-name one. Forever only. Names are split with the client's own Constants.CharacterNameSeparatorConsts.CHARACTERNAME_SURNAME_SEPARATOR (a space only when the constant is absent), mirroring Blizzard's NameUtil.SplitPlayerNameIntoParts, through the new fn.surnameSeparator / fn.splitFullName, which fn.myName now joins with too. Two officers sharing a first name lose the older one's pre-v2.14.9 row -- accepted, as the list is display-only. History's "Mine only" keeps counting a first-name by stamp as ours, with the same accepted collision risk. Spec run red first.

Changed — one answer to "is this me"

Peer Review, thread a08ac06a, finding 5. The UnitName("player") fallbacks beside fn.isCommSelf / fn.myName are deleted: Announce's isSelf, Welcome:OnGuildChat, canEditGuildPolicy and the policy push's setBy in GUI/SettingsPanel.lua, and the last line of policyIsOurs. None was ever taken in production. announce_channels_spec and welcome_spec supply fn.isCommSelf in their fixtures rather than loading functions.lua, whose standalone load needs the whole bootstrap chain; the real one is pinned in zz_forever_names_spec.

[v2.14.9] (2026-09-29) — skip names in other alphabets, and WoW Forever announce and welcome fixes

New — skip names not written in the Latin alphabet

Feature request (Discord, 2026-09-29): "Auto skip characters with non-english characters in their name", with a screenshot of two Chinese names in the compact tray. Settings > General > Scanning now has Skip names not written in the Latin alphabet (DB.global.latinNamesOnly), off by default at the operator's word. The game gives no language for a /who result (Forever's WhoInfo carries name, guild, level, race, class, area, gender and season only), so fn.nameIsLatin judges the character name's script: ASCII, Latin-1, Latin Extended-A/B and Latin Extended Additional pass; any other non-ASCII byte (Han, Hangul, kana, Cyrillic, Thai, ...) does not. A realm suffix is dropped first. It is checked at discovery in fn:addNewPlayer (counted as filtered, after unique) and again at invite time in fn:queueRowFiltered, so a queue built before the switch does not keep inviting those names. It cannot tell Spanish from English, and does not try. Pinned in zz_latin_names_spec.

Fixed — WoW Forever: ADDON_ACTION_BLOCKED on the auto-sent guild welcome

Player report, Forever, 2026-09-29: ADDON_ACTION_BLOCKED on C_ChatInfo.SendChatMessage, raised from flush() inside Welcome:Pump(true) -- the C_Timer auto-send path added in v2.14.6. The Forever client refuses that timer send even for GUILD. autoSend's block watch did catch it and fall back to clicks, but only after the client had raised the error. Welcome:AutoSend() now answers false on gv.isForever, so Forever never arms the timer and every welcome goes out on the next click. Live retail is not known to block it and keeps the setting; that is not verified in-client. Pinned in welcome_spec ("never auto-sends on WoW Forever").

Fixed — "Whisper tabs on the compact tray" could not be unticked

Player report (Vishiswaz, Discord, 2026-09-29): "The button to disable whispers on the compact tray does not allow you to un-check it." The v2.14.8 setter in GUI/SettingsPanel.lua wrote (not v) and false or nil, and x and false or y is always y in Lua, so unticking stored nil, which get reads as on. It is now a plain if. The v2.14.8 spec wrote DB.global.whisperTabs directly and never ran the setter, which is how it shipped; zz_whisper_tabs_spec now drives the Settings checkbox's own get/set both ways.

Fixed — WoW Forever: "Announce to Guild did NOT go out" after the announcement went out

Player report (Vishiswaz, Discord, 2026-09-29): "announce to guild went out but it thinks it didn't", with the post visible in guild chat and FGI's failure line three seconds later. Delivery is confirmed by the post's own echo, and isSelf in Modules/Announce.lua compared the echo's sender with UnitName("player"). On Forever that returns the first name and the surname separately, so "Vishi Swaz" never equalled "Vishi" and every guild post timed out as undelivered.

  • isSelf now asks fn.isCommSelf, the Forever-aware check v2.14.8 added for addon messages.
  • Welcome:OnGuildChat had the same comparison for its own welcome echo, which on Forever read the player's welcome as a guildmate's; it asks fn.isCommSelf too.
  • fn.isCommSelf depended on LibGuildRoster 1.1.1's UnitKeyName to join the two names and fell back to the first name alone without it. It now joins them itself in that case, with the client's surname separator, the way the library does.
  • Constants added to .luarc.json.
  • Pinned in announce_channels_spec (the matcher asks fn.isCommSelf) and zz_forever_names_spec (the full name is recognised without UnitKeyName). The Forever example in announce_channels_spec first failed with the real UnitName stub because that file loads Announce without functions.lua; it now pins the delegation, and the name rule is pinned where it lives.

Fixed — WoW Forever: the rest of FGI knowing the player by their first name only

Swept after the announce fix: every other place that took UnitName("player")'s first return as the player's name. New fn.myName() is the one answer -- the full "First Last" on Forever (through LibGuildRoster's UnitKeyName, or the same join done here without it), UnitName's first return everywhere else -- and fn.isCommSelf now uses it. Moved onto it:

  • the policy-editor check in GUI/SettingsPanel.lua (canEditGuildPolicy), now compared without case, and the Add member box Title-cases each word so "vishi swaz" becomes "Vishi Swaz";
  • the policy's setBy stamp when pushing, and policyIsOurs in GUI/Tabs/Filters.lua, which accepts either spelling so a policy stamped before this release is still recognised;
  • GuildEvents:OnLeave's "is this our own departure" test, through fn.isCommSelf;
  • MemberStats:myKey and its row name, so the By officer line keys a Forever officer by full name;
  • recruit attribution (rec.by) and Recruits:isMine, which still counts a first-name-only stamp as ours;
  • the anti-spam list's Inviter and the blacklist's Blacklister columns.

On every other client each of these reads exactly what it read before. A Forever officer's By officer row stamped before this release stays under the first name; the new row carries the full one. Pinned in zz_forever_names_spec; guild_events_spec's stand-in fn gained the isCommSelf the real one has. Found by a literal search for UnitName("player"), so a site spelled differently was not covered.

[v2.14.8] (2026-09-29) — WoW Forever fixes, and a switch for the whisper tabs

Fixed — WoW Forever: "Hide outgoing whisper echoes" did nothing

Player report, Forever, 2026-09-29. fn.refreshWhisperHideDeadline keys the recipient of each FGI whisper through fn:fullPlayerName, and fn.hideWhisper looks the echo's name up the same way. On Forever one side carried an appended realm and the other did not, so the two keys never met and every echo showed. With fn:fullPlayerName following the regional-names rule (below), both sides are the full name. Pinned in zz_forever_names_spec by driving the real send-then-echo pair.

Fixed — "No player named X is currently playing." repeated in chat, every game version

Player report on Forever, 2026-09-28, three identical lines in four seconds; the operator, 2026-09-29: "it should be fixed in all version to be behind a debug flag" and "it's not only forever, it's all game versions that have this issue".

It is the server's reply to a whisper that could not be delivered, and FGI sends one recruit message as several chunks, so one unreachable recruit printed it once per chunk -- on every flavour.

  • All versions: fn.hideNotFound, a CHAT_MSG_SYSTEM filter registered at load through addon.API.AddMessageEventFilter, hides that line when the name is one FGI itself just whispered (addon._outgoingWhispers), unless debug mode is on. The pattern is built from the client's own localized ERR_CHAT_PLAYER_NOT_FOUND_S. A whisper the player typed to anybody else still shows its error, and FGI's own not-found handling (Modules/Scan.lua) is unaffected -- a chat filter changes only what the chat window draws.
  • Forever only, the cause of every whisper failing there: fn:formatNameForRetailAPI sent "First Last-SomeRealm" whenever the realm was not the player's own internal realm, and the server rejects that. With regional names on it now always sends the full name with no realm. (That the server accepts "First Last" follows Blizzard's own whisper UI; not verified on a Forever client.)
  • Forever, addressed addon messages too: a follow-up from the reporter -- "You were online / When it happened" -- ruled out an offline recruit. FGI's baton and announce-cooldown catch-up replies (Modules/FGI_Baton.lua, Modules/FGI_AnnounceSync.lua) whisper the asker by the name the addon message arrived with, realm included. Both send helpers and addon.API.SendChatMessage now address WHISPER sends through fn.whisperTarget, which drops the realm only on a client with regional names and returns the name untouched everywhere else. fn.whisperTarget delegates to LibGuildRoster's lib:WhisperTarget when the library carries it (1.1.2, thread 54277063 -- the library's own here reply and sister-pull sends had the same fault and now use it), and keeps its own fallback for an older library. DeltaSync fixed its own whisper sends the same way (thread dd611b26). Both library fixes reach players only when those libraries release. Which of the sends produced the reported lines was not established. AceCommQueue and VersionCheck also send whisper-addressed messages and were not looked at.
  • Pinned in Tests/zz_forever_names_spec.lua: hidden for FGI's own target, shown for a bystander, shown with debug on, other system lines untouched, and the Forever send name.

New — a setting to turn the compact tray's whisper tabs off

A player asked (Discord, 2026-09-28): "Is there a way to turn this off in the settings? I prefer the whispers in my normal/general chat feed." There was none. Settings > General > Appearance now has Whisper tabs on the compact tray, on by default (DB.global.whisperTabs, stored as false only when switched off). Off, WT:Enabled() stops everything in Modules/FGI_WhisperTabs.lua: no whisper is recorded, the chat filter keeps nothing out of the game's chat, the open-tab list is empty so the strip hides, and WT:Refresh releases the whisperMode hold. Switching it off takes effect at once, not on the next whisper. Pinned in zz_whisper_tabs_spec.

Fixed — WoW Forever: FGI did not recognise the player as themselves

Player report, WoW Forever, 2026-09-28: the Settings page read "Current designated inviter: Vishi Swaz" to Vishi Swaz, where TBC correctly says "You are the designated inviter right now." The reporter's own conclusion: "since realm names are gone, and the character names cannot be reused in different rulesets for Forever, the appended realm name should probably be abandoned/disabled for Forever".

On Forever a character is "First Last", unique across the region, and UnitName returns the first name and the SURNAME. Two things keyed one character two ways:

  • LibGuildRoster built the player's own key from the first name plus a realm, while the roster had the full name. Its 1.1.1 fixes that (full name, no realm, when the client's RegionalUniqueNamesEnabled() says so); the designated-inviter readout and the election's "is it me?" read the library's key, so that half needs LibGuildRoster 1.1.1 released.
  • FGI's own keys did the same: fn:normalizePlayerName and fn:fullPlayerName appended a realm, and fn.isCommSelf compared against UnitName's first return only. On a client with regional names they now take the library's rule -- the full name, no realm appended, an API-added -Realm dropped -- delegated to LibGuildRoster:CanonName / UnitKeyName, with the same client switch as the fallback. Every other client is untouched: the switch is absent there.
  • Saved keys are repaired at login (Peer Review a04ec605, release blocker). Everything FGI stored on Forever before this release carries the realm it used to append, so a player blacklisted as "First Last-Realm" read as clean under "First Last" and would be invited again. New addon.MigrateForeverKeys, run from addon.MigrateRealmStores on every Forever login, rewrites the hyphenated keys of blackList, blackListRemoved, alreadySended, leave and factionrealm.recruits through fn:fullPlayerName, keeping the newer entry when both spellings exist (and fixing a recruit's name). Every login rather than once, because guildmates on v2.14.7 keep syncing realm keys in; DB.global.migratedToForeverKeys records that it ran. It also stops that function's Classic-family pass, which APPENDS a realm to every bare blacklist key, from running on Forever -- it would have undone the repair on the next login. That pass is barred by the gv.isForever flavour as well as the runtime switch, and the repair runs again from OnEnable (PLAYER_LOGIN), because whether RegionalUniqueNamesEnabled() already answers during OnInitialize is not verified.
  • FGI's fallback copy of LibGuildRoster's Forever rule had drifted (Peer Review a04ec605 F5). A new spec runs both on one list of names: the fallback kept the space in "Name - Realm" and a stray colon. regionalKey now cleans the name step for step as CanonName does.
  • Names cut at the space, swept for Peer Review a04ec605. /fgi template send <n> <player> matched the player with %S+, so a Forever "First Last" was refused as a usage error; it now takes the rest of the line. TemplateWire:Send was the one whisper-channel send not addressed through fn.whisperTarget, and now is. fn:parseName ([^%s-]+) had no caller and is deleted. The realm-strippers ^([^%-]+) in FGI_Welcome, FGI_WhisperTabs and FGI_RealmResolver, and the /fgibl parser, keep a space and need nothing.
  • RegionalUniqueNamesEnabled added to .luarc.json.
  • Tests/zz_forever_names_spec.lua pins the Forever keys and the control (no switch, realm still appended). The name examples were not run red against the old code; the four login-repair examples were, and all four failed.

Fixed — a Lua error on WoW Forever when hovering the world after an FGI tooltip

Player report, WoW Forever, 2026-09-28: LibLocaleOverride-1.0.lua:757: attempt to compare local 'text' (a secret string value, while execution tainted by 'LibLocaleOverride'), from FGI's GameTooltip:Show hook (functions.lua, refontTooltip) under Blizzard's world-cursor unit tooltip. addon.Tooltip.Owner flags the shared GameTooltip as FGI's, and only OnHide cleared the flag, but Blizzard re-owns the tooltip for a world-cursor unit without hiding it. So the flag outlived FGI's tooltip, FGI re-fonted Blizzard's lines, and on Forever/retail those lines can hold secret strings.

  • addon.Tooltip.Owner and the minimap broker tooltip now record the owning frame (addon._ttOwner).
  • The Show hook re-fonts only while GameTooltip:GetOwner() is still that frame; otherwise it puts the line fonts back, drops the flag, and touches nothing else.
  • zz_tooltip_font_restore_spec "never walks a tooltip another frame took over without hiding it first" pins it, and was run red with the owner check disabled.

Fixed — CurseForge listed LibDBIcon as embedded, so the app never installed it

v2.14.7's Forever-only copy of LibDBIcon kept upstream's first line, --@curseforge-project-slug: libdbicon-1-0@. The BigWigs packager reads that keyword in any shipped Lua file and records the library as an Embedded Library on the upload, which contradicts .pkgmeta's required-dependencies. The project page showed LibDBIcon-1.0 as "Embedded Library" (operator's screenshot, 2026-09-28), so the CurseForge app did not install it for the six flavours that need the standalone copy. The keyword line is removed from Libs/LibDBIcon-1.0/LibDBIcon-1.0.lua and a comment says not to restore it; the Forever embed itself still loads. A CurseForge relation belongs to the uploaded file and one zip serves every flavour, so the relation cannot differ per flavour: Required is the one that works for all seven.

After this release uploads, check the CurseForge project's Related Projects: LibDBIcon-1.0 must read "Required Dependency". A harness contract asks for a .pkgmeta checker that would have caught this (WoWAPITesting thread efb04252).

Changed — chat lines nobody asked for are debug-only

At the operator's request (GuildRoster thread cbda57eb: "people are going to whine about this, these types of prints need to be behind a debug on/off flag and a setting in the control panel"):

  • The two "Scan timer restarted" lines in fn.watchForeignWho print only with DB.global.debug on (Settings "Debug mode", or /fgi debug). LibGuildRoster's sister-guild /who triggered them at every login. The button countdown is unchanged.
  • The three keybind "OnClick fired" traces in FGI_Core.lua (F5/Invite, F6/NextSearch, Announce) are debug-only too; they printed on every hotkey press.
  • Every other chat line was read and kept: each answers a press, is already opt-in, reports queued work that was dropped, or answers a slash command.

Changed — packaging and titles

  • .pkgmeta carries no comments at all (operator, 2026-09-28: "can you just take all the damn comments out of the .pkgmeta? they do nothing but cause issue").
  • Every TOC's title is the fleet brand colour: ## Title: |cffFF8000FastGuildInvite|r (DeltaSync thread 8de14b7d).
  • wow-version-replication.ps1 treats a "Deleted" whose source file still exists as a change, so a save-by-replace can never remove a live file from the other installs (TOGProfessionMaster thread eb9d3b3f).

Tests

  • zz_queue_seen_spec: the recheck-driver sweep matched only refreshqueue, while its comment claimed to back every deleted fn name; it now covers queuerefresh, queueseendays and seenpass as well.
  • altgroups_spec: the persistence check is a paired positive (A.Feed present, nothing named store/accept) rather than the module's complete key set, which went red on any harmless new function. zz_recruit_goals_spec keeps its whole-surface check and now says the surface is the contract there.
  • zz_scan_rate_floor_spec and zzzz_core_spec pin the debug-only lines: silent by default, printed with debug on.

[v2.14.7] (2026-09-27) — the minimap button on WoW Forever

Changed — WoW Forever embeds LibDBIcon again, temporarily

LibDBIcon-1.0 is not yet published for WoW Forever (its standalone TOC lists no 16001, and CurseForge does not offer it there), so FGI's hard dependency on it left Forever players unable to load FGI. The operator, 2026-09-27: "i need to temorarily embed libdbicon in the Forever version of FGI ... i don't want it in the other versions just the Forever version".

  • FastGuildInvite_Camelot.toc loads Libs\LibDataBroker-1.1\LibDataBroker-1.1.lua (MINOR 4) and Libs\LibDBIcon-1.0\LibDBIcon-1.0.lua (MINOR 56) first, copied from the standalone LibDBIcon-1.0 v12.0.3. LibStub and CallbackHandler come from Ace3, still a hard dependency there.
  • LibDBIcon-1.0 moves from _Camelot's ## Dependencies to ## OptionalDeps, so a standalone copy still loads first when present and LibStub keeps the higher MINOR.
  • The other six TOCs are unchanged: they embed nothing and still hard-depend on the standalone. The files ship in every zip (one package serves all flavours) but only _Camelot loads them.
  • Tests/zz_toc_manifest_spec.lua pins it: _Camelot loads both files, in order, before any FGI file, and lists LibDBIcon only as optional; no other TOC loads them; both files exist.
  • To undo when LibDBIcon ships for Forever: delete the two TOC lines and the two Libs folders, put LibDBIcon-1.0 back in _Camelot's ## Dependencies, and empty FOREVER_EMBEDS in the spec.

Tests — 74 assertions that pinned nothing now pin their key

writ's spec-shape check (inbox 3e7ade77) found 74 is_nil assertions on a literal key across 35 spec files: a misspelled or renamed key would have passed them forever. Each now derives its key the way the code does, pairs the nil with a positive assertion on the same key before the removing action, or compares the table's complete key set. No new assertion failed, so no production defect was found.

[v2.14.6] (2026-09-26) — welcomes send by themselves again, whisper tabs that keep your place, and recruitment goals that hold invitations only

Changed — a recruitment goal holds invitations only, says so once, and can exempt ranks

Reported on Discord by Vishiswaz, who asked for the goal, with a screenshot of the "Recruiting stopped" line every 30-60 seconds: "It should either be one message every hour or X long amount of time, or it should be just one message one time when it hits the recruitment goal, and not message again unless the recruitment resumes and the goal gets hit again. Also, it should be tied to the Invite Candidates function of Wingman. I have that disabled right now, so I should not be getting this. When I recommended the function, I did not mean for it to affect announcements or anything else, but as a function of wingman." And: "Is it possible we could have a guild policy that allows people from X rank and above to override the guild recruitment goal policy with their personal policy? So I as an officer can set limits on non-officers but not myself?"

  • Invitations only. Modules/Wingman.lua no longer stops the whole drain at a met goal; the check moved into tryInvite, which returns false so the click falls through to announce or scan. With the Invite step off, the goal is never consulted and nothing prints. The scan gate in fn:nextSearch (functions.lua) and the announce gate in Announce:Send are removed, along with the now-unused announceMsgGoalReached / announceMsgPersonalGoalReached strings. The Invite button's gate in fn:invitePlayer stays and still answers every press.
  • Said once. Modules/FGI_RecruitGoals.lua replaces the 30-second NOTICE_EVERY throttle with a noticed flag: an unasked notice (Wingman's) prints once, and Goals:Blocking() clears the flag the first time it finds the goal no longer met. A deliberate press (explicit) is still answered every time, and also sets the flag.
  • Ranks exempt from the guild's goal. New policy field gmPolicy.goals.exemptRank (0-based rank index, like editors.minRank), authored in Settings, Guild Policy, "Ranks that follow only their own goals". Goals:Exempt() reads GetGuildInfo("player")'s third return; for an exempt rank Goals:Targets() ignores the guild's targets, so only the member's own goals apply. It is policy, set by whoever may edit policy and pushed to everyone, so the 2026-09-09 no-bypass rule stands.
  • Settings text for the goals section now says invitations stop and scanning and announcements carry on. The personal-goals text no longer says a personal goal can never let you recruit later than the guild's, which is false for an exempt rank (Peer Review, thread bcd42b14).
  • Known cost: a guild at its goal keeps advertising and scanning, so players who answer an ad whisper and get no invite. That is what was asked for, and the player notes say it.

Specs: Tests/zz_recruit_goals_spec.lua gains a rank-exemption block and "once per time the goal is reached" examples. The scan and announcement examples now assert they are NOT held, and the Wingman examples assert the invite step is held while the click scans, and that nothing prints with Invite off. FGI_RecruitGoals.lua is at 100% line coverage. Not verified in a client.

Fixed — an FGI /who could switch an open Social window to its Who tab

GuildRoster, thread c5a1a43e. Libs/LibWho/LibWho.lua's BorrowWhoList left Blizzard's WHO_LIST_UPDATE registration in place whenever a Who-list owner was VISIBLE. With FriendsFrame open on its Guild tab (or GuildRoster's Sisters page inside it) that handed the handler back while our /who was out, and the answer ran WhoList_Update, which ends PanelTemplates_SetTab(FriendsFrame, 2); ShowUIPanel(FriendsFrame) (Classic Era FriendsFrame.lua:364-366, :1003-1004). New whoPaneOf(owner): for FriendsFrame the Who tab's sub-frame WhoFrame (FriendsFrame_ShowSubFrame("WhoFrame"), Classic :479, retail Mainline :489), any other owner itself. The borrow is skipped only while that is visible, and the debt-settling OnShow hook sits on it and re-checks it when it fires, so opening Social on another tab no longer settles the debt; turning to the Who tab does. Three examples in Tests/zz_who_libraries_invariant_spec.lua. Not verified in a client.

Changed — Wingman asks GuildRoster whether a /who is owed

fn.foreignWhoOwed calls GuildRoster 0.10.0's lib:IsWhoOwed() when it exists, and keeps the whoSync.queue read for an older library. IsWhoOwed leaves out the library's new location queries, which alternate with other senders on their own, so they do not take Wingman's scan clicks.

Fixed — /fgi resetSettings emptied the History tab

fn:resetSettings (functions.lua) promises to keep the records, and its keep-list carried the history event log but not DB.factionrealm.recruits, which is what the History tab reads (Modules/FGI_Recruits.lua), nor DB.factionrealm.memberStats, the Statistics tab's By-officer line (Modules/FGI_MemberStats.lua). Both are on the keep-list now, and the completion line names them. Found by the v2.14.6 docs audit. Tests/zz_reset_settings_spec.lua gains an example for the two.

Changed — docs brought in line with the addon, and five stale in-game texts

A code-against-docs audit of README.md and docs/Curseforge_Description.html corrected, among others: the designated duty lists need Enable guild policy; Wingman's locks follow the steps it runs; its auto-stop is "may neither announce nor invite"; /fgi version prints the game version only; /fgi queue lists what would be dropped; the blacklisted-member check is off by default; multi-select invites go one per click; Wrath and Cataclysm Classic borrow another version's race/class lists. /fgids is now documented. The Mute announce skip notices tooltip no longer lists "chat queue busy", a message removed with its gate (Locale/enUS.lua, GUI/SettingsPanel.lua).

Four more in-game texts were wrong and are corrected in Locale/enUS.lua: the Hotkeys header said the keys never touch WoW's binding system (they are set through SetBindingClick); the member-history header said both tooltip toggles default on (the chat-name one is off); the Wingman minimum-interval tooltip named the old "Scan interval"; and the blacklist-check description offered a Skip button that was removed. Where the MEANING changed (the hotkey and member-history headers), the key changed too, so the other locales' now-wrong translations are orphaned and those players see the correct English until a translator updates it (Peer Review, thread aa0a1e73). The Wingman tooltip only renamed a setting, so its key stands and existing translations keep working; the blacklist-check text is a token key found only in enUS.

Tests — four more file-order dependencies, found by shuffled runs

  • Tests/support/addon.lua M.initDB builds a database when FGI.DB is not a real AceDB object (it used to accept any table with a global field, so Tests/guild_events_spec.lua's stub could survive). On an existing database it puts back any DEFAULT an earlier file deleted, without replacing a table that exists. Seed 1 ran every status-bar example without factionrealm.history.
  • Tests/zz_tabs_render_spec.lua and Tests/zz_history_counters_spec.lua create the history series they read. Tests/zz_scan_groups_data_spec.lua sets the level range its standalone bucket needs.
  • Tests/zz_whisper_tabs_spec.lua's fire raises an existing zero-attempt recruit record to one attempt, and puts it back afterwards (seed 12345).
  • The full suite passes in forward, reverse and shuffled order (seeds 1, 42 and 12345).

Changed — whisper tabs: only players FGI invited, oldest on the left, and clicks on them are not Wingman clicks

Reported on Discord by crimsonmane: "I have to stop wingman if i want to scroll through the whispers, even closing any whisper tabs is futile because it's a mouse click which triggers another whisper ... Currently this area captures any whisper, not just ones triggered by FGI." The operator's answer, on 2026-09-26: "can we make it so we're only capturing whispers of the people that we've invited, not all whispers? can we also make it so the 'oldest' whispers are on the left, so new whispers don't 'push' the whispers you're using off the screen? would it be possible to exclude clicks here from the action of wingman?"

  • Modules/FGI_WhisperTabs.lua WT:Record now records a whisper only when WT:IsRecruit finds a record in fn.recruits with attempts > 0, meaning this character sent the player an invite or whisper through FGI. A bare record is not enough (Peer Review, thread cae6b6fb): onJoined writes one for every guild join and acceptRemote for every record a guildmate syncs, both with attempts = 0, and onAttempt is the only writer of attempts. A player invited outside FGI (right-click Guild Invite, /ginvite) gets no tab. The gold "recruit" colour and tooltip line no longer mark anything, so the tooltip line is gone. This replaces v2.14.3's "every whisper gets a tab".

  • WT.order is now the order conversations opened, oldest first. touch() became enter(), which appends a new conversation and never moves an existing one. The cap drops the oldest conversation that is neither open nor unread. A response no longer jumps to the first slot or resets the scroll (the 2026-09-24 rule it replaces). A hidden tab with unread lines still flashes its arrow.

  • A conversation's FIRST reply now moves its tab left, to just after the conversations already answered (a new replied flag on the conversation). Answered conversations lead in the order they were answered, and unanswered ones follow, oldest first. The operator's words: "it shouldn't go to the absolut left, it should go left, but be to the right of any other active conversations". Later replies and the player's own lines move nothing.

  • No limits on conversations or lines. The operator asked, 2026-09-26: "why is there a tab limit? is that some arbitrary number?" It was: MAX_CONVOS = 12 and MAX_LINES = 50, both picked, and both justified by a SavedVariables risk this session-only module never had. Their follow-up: "you need to remove those ... this is OUR ADDON ... we have our OWN window". Both are gone. enter() only appends, and paintThread sizes the thread box (ScrollingMessageFrame:SetMaxLines, a Lua history buffer per Blizzard's ScrollingMessageFrame.lua:240) to the whole conversation. Extra tabs scroll behind the arrows.

  • Lines read like the game's chat, with class-coloured names. paintThread formats each line through CHAT_WHISPER_GET / CHAT_WHISPER_INFORM_GET (as Blizzard_GMChatUI.lua:98 does), so it shows [Name] whispers: / To [Name]: as a |Hplayer: link. The name is coloured from CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS once the conversation's class is known. WT:Record learns it once from the event's GUID via GetPlayerInfoByGUID (second return, the English class token), so a conversation with only outgoing lines shows the name uncoloured.

  • Each arrow shows how many tabs are hidden on its side (< 3, 2 >). ARROW_W went from 16 to 30 to fit the count.

  • The strip, the conversation pane and the player menu sit outside the compact tray's rectangle, so mouseOverOwnUI in Modules/Wingman.lua never saw them, and every click there drained a Wingman step. The new WT:IsMouseOver() covers all three, and mouseOverOwnUI asks it.

  • The game's own whisper window still opened after a few exchanges (operator, 2026-09-26: "i still got a popup in the old chat box after a few back and forth", then "it has the conversation in both places now. i was hoping to capture all of one conversation into the FGI window UNTIL the FGI tab is closed"). Three changes, all keyed on the new WT:Captures(name): a player FGI invited whose tray tab has not been closed, and only while the compact tray is shown, since with it hidden the conversation would appear nowhere.

    • WT.chatFilter, registered for CHAT_MSG_WHISPER and _INFORM through addon.API.AddMessageEventFilter, keeps every captured line out of every game chat window.
    • While any conversation is captured, WT:Refresh holds whisperMode at inline through the new fn.holdInlineWhisperMode("whisperTabs"), on every flavour. It gives the player's value back through fn.releaseInlineWhisperMode when the last tab closes, the tray hides, or at PLAYER_LOGOUT. This was the actual cause. On the Classic family the FCF_OpenTemporaryWindow hook closes the window for our own send, and then FloatingChatFrameManager_OnEvent calls FCF_SelectDockFrame and FCF_FadeInChatFrame on it, which show it again (Classic FloatingChatFrame.lua:2354-2361). Only prevention works there. There is no timer and no echo counting: a first version counted echoes back through the send path's C_Timer restore, and Peer Review (thread 0aa18e31) ruled that out under the operator's rule against timers that call the WoW API. The send path's own restore defers to the hold. Known cost: while a conversation is captured, a whisper from anyone else shows in the chat window instead of popping out.
    • The hook also closes a window opened by an incoming line of a captured conversation, where nothing re-shows it. I first blamed a window opened before the change loaded; the operator confirmed it was not open.
    • Closing a tray tab now hands the conversation back to the game's chat for the session: the next whisper no longer reopens it, which replaces the 2026-09-24 rule.
    • Retail: the filter and the whisperMode hold apply there too, so no game window opens for a captured conversation. The only exception is the very first incoming line from a recruit who has no tab yet, which arrives before the hold is taken. Not verified in a client.
  • WT:IsRecruit now tries the name as given, Ambiguate(key, "none") and fn:fullPlayerName. The recruit store keys by fn:normalizePlayerName, which leaves a Classic name bare, while the tabs and the hook pass Name-Realm, so a single lookup missed same-realm recruits.

Specs: Tests/zz_whisper_tabs_spec.lua (a stranger gets no tab, oldest-first order, a response moves nothing, a new conversation joins on the right, IsMouseOver per frame) and Tests/zz_wingman_session_spec.lua (a click over the tabs does not pump or drain). Not verified in a client.

Changed — no timer restores whisperMode any more

Peer Review (thread 0aa18e31, finding 2), under the operator's standing rule against a timer that calls the WoW API. The retail send path's fn.suppressPopoutForSend / fn.notifyEchoSuppressed restored whisperMode through C_Timer: a one-frame C_Timer.After(0) after the last echo, and the checkWhisperModeFallback checker at 60 s for echoes that never came. Both ended in SetCVar. Both are gone. The last echo now only marks the restore due, and the new fn.settleWhisperMode, called first in fn.pumpClick, performs it on the player's next click, or once WHISPER_MODE_FALLBACK has passed. The old one-frame deferral protected against the manager seeing the final echo after our filter, and a later click is a later frame by construction. Cost: whisperMode stays inline from the last echo to the next click, so a reply in that gap shows inline. PLAYER_LOGOUT still restores regardless. Tests/zz_whisper_popout_spec.lua now drives the restore by click and asserts no timer is armed.

The whisper tabs' WT:Refresh no longer takes its whisperMode hold from the tray's own OnShow or OnSizeChanged. The settings gear re-shows the tray from C_Timer.After(0) (Modules/compactFrame.lua, cf.gearBtn), which would have put SetCVar in a timer. The hold is retaken on the next whisper or click on the tabs.

Changed — welcomes send by themselves again once the delay is up (DB.global.welcomeAuto, on)

DADDY on Discord, 2026-09-26: welcomes were "still working on clicks"; the operator: "they don't want a click, they want it to go out after a short delay like it did before, why can't we do that again as an option?"

v2.13.0 was wrong about why it removed the timer send, and I wrote it. It read HasRestrictions = true on SendChatMessage (Era ChatInfoDocumentation.lua:403-406) as "no chat send without a click, on any flavour", and moved every welcome onto the next click. The client's rule is narrower: only SAY, YELL and CHANNEL need a hardware event; GUILD and WHISPER do not (warcraft.wiki.gg API_SendChatMessage; the harness's enforceChat models the same). What raised the ten ADDON_ACTION_BLOCKED in the v2.13.0 field report was never established; retail's chat-messaging lockdown is the likeliest candidate, not verified. Earlier this session a timer send was built and reverted under the operator's no-timers rule (Peer Review, thread 3eefec1c); the operator's words above are the explicit exception for the welcome.

  • Modules/FGI_Welcome.lua: new Welcome:AutoSend() / NextDueAt() / ScheduleAuto(). Every queue change arms ONE C_Timer.After for the earliest due welcome (a token discards an outdated one, so a late joiner pushing the grouping window out is honoured); it calls Pump(true). Whispers go one per run on both paths, and the timer spaces auto sends AUTO_SPACING (1 s) apart, so a big intake is not whispered all at once (Peer Review, thread f3981466). In chat-messaging lockdown nothing is sent and the timer looks again every 10 s (a poll: no lockdown-ended event was looked for). ClickDelivery() now answers not AutoSend(). The click path is unchanged and still delivers early.
  • If the client blocks an auto send anyway (Peer Review, thread f3981466): ADDON_ACTION_BLOCKED is SynchronousEvent = true with the addon name first (Era RestrictedActionsDocumentation.lua:71), so a blockWatch frame sees it inside the send. The welcome is put back in the queue, the session falls back to click delivery, and one chat line (welcomeAutoBlocked, enUS) says so. It lasts until /reload.
  • FGI_Core.lua default welcomeAuto = true; Settings > Guild's v2.13.0 description ("Welcomes go out on your next click") is a toggle again, "Send welcomes automatically". Added to the settings template catalogue. The pre-v2.13.0 welcomeOnClick key is still not read.
  • A click-anywhere "catcher" frame was written first in this session for recruiters without Wingman, and removed before it ran: the operator does not want a click at all.

Specs: Tests/welcome_spec.lua gains an auto-send block (sends with no click, every due whisper in one run, a late joiner re-arms and the outdated timer sends nothing, held in lockdown then sent); the click-delivery block runs with welcomeAuto = false. Tests/zz_protected_welcome_spec.lua adds two examples under the harness's chat rule proving the timer's GUILD and WHISPER sends are delivered with no click, and its blanket-guard pair now says that guard is stricter than the client. FGI_Welcome.lua 100% line coverage. Not verified in a client.

Fixed — a welcome waiting to be sent is no longer dropped after ten minutes

Reported on Discord (DADDY / Vishiswaz): "I got a message that the welcome message timed out since my last button push ... i do not want it to miss people just because i do not hit a button." Modules/FGI_Welcome.lua queues every guild welcome and welcome whisper for the click pump, and Pump dropped any entry older than WELCOME_QUEUE_TTL (600 s) with a chat notice. A player who stepped away lost every greeting owed while they were gone.

Now nothing is dropped for age alone. WELCOME_QUEUE_TTL is replaced by WELCOME_STALE (600 s), which only makes the click ask LibGuildRoster whether a stale welcome can still land: a stale guild line leaves out a joiner the roster no longer has (a leaver seen live was already removed through CancelFor), and a stale whisper to a member the roster shows offline is dropped instead of sent, without spending the click's one message. With no ready roster the welcome is kept. The notice (welcomeQueueExpired, enUS) now says what it means and no longer names the "Send welcomes on a click" setting v2.13.0 removed. The key exists only in enUS.

Tests/welcome_spec.lua: the example that asserted the ten-minute drop is replaced by the regression guard (queued, twenty minutes with no click, still pending, one click sends it once), plus stale-whisper-offline, stale-joiner-left and roster-not-ready examples. FGI_Welcome.lua coverage 264/264.

Not verified in a client.

Fixed — FGI's /who could stay "quiet" after the player opened the Social pane

Found by the shuffled-order suite (--order shuffle:1), not in the field. Libs/LibWho/LibWho.lua's BorrowWhoList hooked the OnShow of only the Blizzard panels still registered for WHO_LIST_UPDATE. When GuildRoster's sister-guild /who had already borrowed FriendsFrame's registration, FGI's query saw no panel, borrowed nothing and hooked nothing -- so the player opening the Social pane settled GuildRoster's debt and left FGI's Quiet standing. It had passed in file order only because an earlier spec hooked the same frame. FriendsFrame is now hooked on every borrow; on the Classic family it is the only Who host. LibWho_Retail.lua was not examined for the same gap.

Fixed — Announce's secret-string guard failed open when its helper was missing

Modules/Announce.lua checked chat payloads with if fn and fn.isSafeString and not fn.isSafeString(x), which skipped the check whenever the helper was absent and let a non-string reach :match. A local isSafe now falls back to type(s) == "string". Found by the reverse-order suite, where another spec's copy of the module (on a stub FGI with no isSafeString) received a table as a channel name.

Tests — the suite passes in any file order

Reverse order and shuffled seeds 12345, 1 and 42 are all 3664/0, from 13 failures (reverse) and 7 / 11 / 1 (the seeds). Every fix is in the spec that leaked state or failed to declare what it reads: the quiet-zone and instance-map caches, the Fairbanks realm two roster specs left behind, Wingman's global overlay name, the scan tab's module-level widgets, a leftover scan cooldown, an enforced guild policy, the main window's one-shot reopen frame, and a dialog example that broke its anchor before a release hook could restore it. The _G restore audit (Peer Review 13c69367) converted 16 skippable inline restores to finally() and fixed one teardown that nilled harness-owned chat-filter globals. FGI_Core.lua coverage is back to 100% with an example for the full-reset reload (:971).

[v2.14.5] (2026-09-25) — personal recruitment goals, a scan list that keeps its place, and the Oxford comma

New — personal recruitment goals

Requested on Discord by Vishiswaz: "Recruitment goals, but on a personal setting also. Whichever is stricter, personal or guild-enforced, is the one FGI enforces." Settings > General > My recruitment goals sets a per-character (DB.char.goals) total and online target. Modules/FGI_RecruitGoals.lua gains Goals:Personal() and Goals:Targets(), which takes the LOWER positive target for each kind (guild on a tie), and Goals:Met() now judges each kind against that and reports source ("guild" / "personal"). A personal goal can only stop a recruiter sooner, never later, so the guild policy's no-bypass rule is unchanged, and it still applies when the guild policy is off. A personal stop is always the chat line (the guild's pop-up setting governs only the guild's goal), worded "your own goal" (recruitGoalPersonalTotal / recruitGoalPersonalOnline), and a held announcement says the same (announceMsgPersonalGoalReached). The panel shows which target is in force and whose it is. Pinned in Tests/zz_recruit_goals_spec.lua ("a recruiter's own goal beside the guild's"). char.goals has a default ({}) in FGI_Core.lua and is in the settings-template catalogue (Modules/FGI_SettingsProfile.lua, group "duty"), because every setting is offered to the template author; it is not synced, so the sync exclusion does not apply.

Changed — FGI's own RowList is gone; every list is LibAceGUIWidgets'

GUI/RowList.lua carried FGI's original list (~900 lines) as a fallback for a player whose LibAceGUIWidgets was older than MINOR 34, the first to carry every field FGI's tabs pass. It stayed through v2.14.4, one release after the library shipped MINOR 34, and is deleted here. What remains is the translation layer (hidden -> show, the class columns as format, the per-list refontHook and tooltipOwner). A library older than MINOR 34 is now an error naming the fix ("Update LibAceGUIWidgets") rather than a quiet fallback. The same text is also printed once in chat from OnEnable (RowList.WarnIfLibraryTooOld), because a Lua error reaches only players who show them and everyone else would see an empty tab (Peer Review 61cd8c48). Tests/zz_rowlist_scroll_spec.lua runs once, against the library, and pins the error and the chat line.

Fixed — the Full Window scan list jumped back to the top on every scan tick

Reported on Discord by Vishiswaz. ScanTab.Refresh(preserveScroll) treated a missing flag as "reset the scroll to row 1", and the engine's live-refresh hook (addon.MainWindow.refreshScanTab, fired from functions.lua on every /who batch and counter change) calls it with no arguments -- so a player scrolled down a filling list was thrown back to the top every few seconds. The flag is now read as preserveScroll ~= false: the list keeps its offset (the library clamps it to the new length) unless a caller explicitly passes false. No caller on the tab wanted the reset. Pinned in Tests/zz_scan_strip_actions_spec.lua ("the live refresh while a scan fills the list").

Fixed — combined welcomes now use the serial (Oxford) comma

Reported on Discord by Vishiswaz. Modules/FGI_Welcome.lua's joinNames joined three or more names as "A, B and C". A new client-locale key, welcomeNameSerialJoin (", and "), joins the last of three or more; two names still use welcomeNameFinalJoin (" and "). The serial comma is English: because fn.chatLoc flattens the client locale over enUS, the enUS serial joiner is used only while the final joiner is still the English " and " -- a locale that translates the final joiner and not the serial one gets "A, B und C" (Peer Review, thread fa0cb971). NamesFromPeerWelcome splits on the serial joiner first -- splitting on " and " alone would read "B," as a name and miss the peer suppression -- and falls back to " and ", so a peer still on v2.14.4 is read correctly. Pinned in Tests/welcome_spec.lua.

[v2.14.4] (2026-09-25) — the scan waits the full timer you set

New — DB.global.scanRespectTimer, on by default

Reported on Discord (Vishiswaz, with a video): "Cooldown timer for scans set to 10 seconds, but it skips down to like 4 seconds." It was working as designed and the design was wrong for the player. Since v2.13.0 the button's countdown starts at the slider value (fn.getGiveUpAfter, the give-up deadline) on the send, and when the answer lands searchWhoResultCallback replaced it with what was left of the server's /who limit (fn.scanFloorRemaining, 5 s Classic family / 8 s retail). An answer in under a second turned 10 into ~4. The operator: "a setting that is turned on by default to not do the 'shorten' thing, it just respects the timer folks set."

  • On (default): the slider value is also the least time between two of FGI's scans, measured from FGI's own send. fn.scanFloorRemaining now returns the larger of the server's remainder and the user timer's remainder, so the answer path, the press gate and the foreign-/who countdown all hold to it without a second code path. Wingman's scan step reads the same countdown flag, so it paces to the timer as well.
  • Off: the v2.13.0 behaviour, server limit only.
  • Two clocks. The server limit is measured from the later of FGI's send (lastSentAt) and a /who sent by the player or another addon (lastForeignSentAt, new; fn.watchForeignWho used to stamp lastSentAt). The user's timer is measured from lastSentAt only, so a sister-guild lookup does not restart the player's pace.
  • The write-off still re-issues on the press that wrote a query off: the timer is the give-up threshold, so at waited >= giveUpAfter its remainder is exactly 0.
  • fn.scanRespectsTimer() answers ON for an unset value. fn.scanFloorReason() says which limit is holding the press, and the refusal line uses it: Scan held: waiting out your scan timer (%ds). for the user's timer, the existing server line otherwise. The debug line names which one too.
  • Settings > General > Scanning: new toggle Wait the full timer between scans under the slider; the slider is renamed Scan timer (seconds) (was "Give up on a /who after (seconds)") with a tooltip that describes both jobs. The setting is in the settings-profile catalogue. The new and changed strings exist only in enUS.lua, as the ones they replace did, so other locales fall back to English for them.

Fixed — /fgi resetDB now does what the Anti-Spam tab's Clear All does

The slash command reassigned DB.realm.alreadySended = {} and left addon.search.tempSendedInvites alone, so a cleared name stayed filtered out of scans until /reload; Clear All emptied the table in place and flushed that cache. Both now call the new fn.clearAntiSpam (functions.lua), which also redraws the Anti-Spam tab if it is open. The README and CurseForge page said the command made "everyone invitable again"; they now say it empties your own list and that guildmates can sync entries back. Bridge:ApplyEntries writes any missing guild:alreadySended name and this list has no removal record, unlike the blacklist's tombstones. The Anti-Spam tab's Clear All help line says the same (new enUS key; the old hiIN translation is removed, so Hindi shows it in English).

Peer Review follow-ups (thread f60a4dd2)

  • Wingman paces at the user timer. Its scan step waits on the tray's countdown, which now includes the timer, so a player who never touched the slider (default 10) scans about every 10 s on Classic instead of about every 5 s. That is the operator's instruction applied as written.
  • A refused Wingman scan no longer spends the click. fn:nextSearch now returns true on the one line that sends; Wingman's tryScan returns that, so a press the timer or the server limit refuses falls through to invite or announce.
  • The foreign-/who "Scan timer restarted" line prints only when fn.scanFloorReason() is "server". With the user's timer longer, that /who changed nothing the player waits for.
  • zz_scan_rate_floor_spec restores subdivideLvl, which it had left off for later files.

Tests

  • zz_scan_rate_floor_spec: a new block for the timer (default on, full countdown after an instant answer, held past the server's limit, released at the timer, never faster than the server, the refusal names the timer, a foreign /who does not restart it, the write-off retry is not blocked). The file's existing examples pin the server limit and now run with the setting off.
  • zz_settings_panel_spec: the toggle reads on when unset and writes through to the gate.
  • zz_scan_inflight_gate_spec, zz_scan_passes_spec, zz_compact_scan_press_spec run with the setting off, because they step over the server's limit on purpose and are not about pacing. zz_libwho_restore_spec and zz_who_libraries_invariant_spec clear lastForeignSentAt along with lastSentAt, since they are the files that drive a player's /who.
  • 3647 passed, 0 failed, in file order and in shuffled order (seed 12345).

Removed — the embedded ChatThrottleLib

Libs/ChatThrottleLib (v29) never ran: Ace3, a hard dependency, loads v32 first. Dropped from all seven TOCs; the one spec that loaded it now loads Ace3's copy.

Changed — LibDBIcon is a dependency on WoW Forever too

The operator: "lets also make libdbicon an external dep". _Camelot now lists LibDBIcon-1.0 in ## Dependencies like the other six, and Libs/LibDBIcon-1.0 and Libs/LibDataBroker-1.1 are deleted. It had kept the embedded copies because the standalone's TOC does not list Interface 16001, but Ace3 (already required there) does not list it either, so the exception protected nothing. zz_toc_manifest_spec now pins that no TOC embeds them; the test loader reads the installed standalone LibDBIcon-1.0 copies. Not checked on a Forever client.

Removed — /fgi seedbl

A dev helper that wrote thirty invented "Name-Realm" bans through the real fn:blackList. Any player could type it, and the blacklist is shared with the guild, so it put made-up bans (names that may belong to real players on the realm) into every guildmate's list. Removed rather than gated behind debug mode, because debug mode would not have stopped the sync. Tests/zz_slash_seedbl_spec.lua now pins that the command adds nothing.

Docs — release prep

docs/Curseforge_Description.html and Modules/intro.lua carry the v2.14.4 notes (New / Changed / Fixed, /fgi seedbl as a Removed bullet). README gains a How fast it scans paragraph (the scan timer and its toggle) and no longer lists ChatThrottleLib among the copies FGI carries. Parity was checked against today's changes only: the full docs-versus-code audit ran earlier on 2026-09-25.

A second full audit (README and the HTML feature sections against the code) then found and fixed: HTML still saying Forever carries its own LibDBIcon and that the addon compartment is retail-only; README and HTML saying Ask at login when I'm the designated recruiter "turns the login prompt off" (it is on by default; unticking it stops the prompt); the README naming Seen again after without "(hours)"; and /fgi kbstate and /fgi escstate missing from the README table. In-game text fixed on the way: Scan tab help (the scan timer, the counter list, Level range source under General → Scanning), Filters help ("every active filter" contradicted its own OR rule), Custom Scan's Strict help (splits follow the subdivision settings, not "class then race"), and the Settings tooltips for Level range (splits where the players are, not always in half) and Mute announce skip notices (a horn press always explains itself). Their old keys stay orphaned in hiIN and the other locales, which show English for these lines until retranslated. Not verified: whether /fgi resetDB's "everyone becomes invitable again" holds when guildmates' synced lists refill it.

Docs — the v2.14.3 Peer Review wording fixes

Peer Review found three README/CurseForge overclaims in the v2.14.3 release prep, and I shipped v2.14.3 without applying them. Applied now:

  • Copy names covers the Blacklist, Anti-Spam, History and Scan tabs only (GUI/UI.lua), and it copies the whole list, not what a search is showing.
  • Addon compartment is "retail and WoW Forever", not retail only: Blizzard_Minimap/Mainline/ AddonCompartment.lua is in wow-ui-source-forever, and FGI's toggle only checks that the compartment exists. The Settings tooltip still says "Retail only"; that is a translated string and is on the todo list with the other stale help text.
  • The welcomer fallback to the announcer list applies only when an announcer list is set.

Fixed — in-game help text that described settings which have moved or gone

Found by the 2026-09-25 docs audit; each claim re-read in code before rewording.

  • Anti-Spam tab help: the expiry lives in Settings → Advanced → Anti-spam memory, not "Settings → Main" (GUI/SettingsPanel.lua, antiSpamExpiryDays).
  • Custom Scan help: the default sweep's level range is the Scan tab's readout or the filters' range (Level range source), not "Settings → Main → low/high level range" (GUI/Tabs/Scan.lua writes lowLimit/highLimit).
  • Quiet Zones help said the list "pauses scanning". It does not: players found there are skipped at ingestion, and the zone step leaves the zones out only with quietZonesSkipWho on.
  • Race/Class chart help and the tab's header comment said the table is "maintained by hand". It is generated from the game's CharBaseInfo data per product branch (functions.lua, the header above RaceClassCombo).
  • Wingman "Post announcements" and the Scanning header tooltips named an "Announcement spacing" setting that no longer exists, and described staggered bursts. A Wingman click that posts sends one announcement to one channel and otherwise falls through to the next step (Modules/Wingman.lua, tryAnnounce). The Scanning header also now describes the scan timer.
  • The addon-compartment toggle's tooltip says "Retail and WoW Forever only".
  • The compact tray's "i" and gear tooltips were bare English strings; each paragraph is now a locale key (texture escapes kept outside the keys), so they can be translated (Peer Review cd121cc9 item 4). English only until then.
  • That tooltip's height hint was an unmeasured 480 px; measured in the client at 514 px, so it is now 520 and both comments state the measurement (Peer Review cd121cc9 item 5). The hint only picks above-or-below placement, so 480 could put the tooltip above the tray without room for it.
  • These are locale keys, so the new wording is English everywhere until translated: the Scanning header and Wingman announce tooltips had translations in every locale (now orphaned keys there), and the three MainWindow lines had Hindi ones, which were removed with the old keys.

[v2.14.3] (2026-09-25) — whisper tabs above the compact tray, LibAceGUIWidgets becomes a hard dependency, and the suite passes in a shuffled order

New — whisper tabs above the compact tray

Modules/FGI_WhisperTabs.lua is new, and it is in all seven TOCs straight after Modules/compactFrame.lua. It puts a row of chat-style tabs along the top edge of the compact tray, one per whisper conversation. Clicking a tab opens that conversation in a pane above the tabs, with a reply box, and clicking it again folds the pane away. The operator asked for it so that their chat and their recruiting are "all in the same place", instead of in new tabs on the general chat box.

  • Which whispers: every whisper conversation gets a tab, not only recruits. A player with a recruit record (fn.recruits) gets a gold tab. FGI_Recruits gained Recruits:get(name), a lookup that never creates a record, so the tabs can check any player without turning a stranger into a recruit.
  • Unread and closing: a tab shows its unread count. Its x closes it, and the next whisper from that player opens it again with the history intact.
  • History: held for the session only and capped at 50 lines per conversation and 12 conversations; the least recently active is dropped. Nothing goes to SavedVariables.
  • Listening: through addon.API.RegisterChatEvents, so nothing is recorded during retail chat lockdown.
  • Replying: from the reply box's Enter, through addon.API.SendChatMessage, after the lockdown check and the retail own-realm name trim. It deliberately does not go through fn:sendWhisper, which would hide the player's own reply as a recruitment echo and, on retail, flip whisperMode. The reply is recorded when the server echoes it back, so a whisper that fails to send never appears as sent.
  • Left alone: Blizzard's own whisper tabs are untouched. On retail they can only be changed through the global whisperMode setting, and FGI must never close or wrap Blizzard's chat tabs. Battle.net whispers are not included.
  • More tabs than fit, flashing, and reordering (operator, after the first in-game look: "i need them to disappear and we have a < and > arrow on either side so we can scroll through them ... the active conversations with response flash like in the main chat window and they should reorder to the 'first' slot on the left"). Each tab is now as wide as its own name (48 to 140), not an equal share of the strip, so names are no longer cut to Felon.... When they do not all fit, < and > appear at the ends and the tabs past the edge are hidden; each click moves the first shown tab by one, and each arrow greys out at its end. A tab with unread lines pulses the game's own chat-tab glow (Interface\ChatFrame\ChatFrameTab-NewMessage, the texture FloatingChatFrame.xml gives a chat tab's glow) through its own animation group, not UIFrameFlash, until it is opened. An incoming whisper moves its conversation to the first slot and scrolls the strip back to it. The player's own reply does not move the tab they are typing in. Two follow-ups from Peer Review (thread 9ab1feae): an arrow pulses the same glow while a tab hidden on its side has unread lines, so an unread conversation scrolled out of view is never invisible; and over the 12-conversation cap, the one dropped is the least recently active that is neither open nor unread, falling back to the oldest only when every conversation is one of those.
  • Right-click a name for a player menu (operator: "i need to be able to right click on the name in the box, and the name in the chat to get the same menu in the normal chat box, so i can invite"). A right-click on a tab, or on a name in the conversation pane, opens FGI's own menu at the cursor (WT:ShowMenu): Whisper (focuses this conversation's reply box), Invite (C_PartyInfo.InviteUnit, through fn:formatNameForRetailAPI), Guild Invite, Add Friend, Ignore (C_FriendList.AddFriend / AddIgnore), Blacklist, Unblacklist and Cancel. Guild Invite, Blacklist and Unblacklist are the same functions the chat menu's FGI submenu calls, now shared as addon.ChatMenu.GuildInvite / Blacklist / Unblacklist in Modules/FGI_ChatMenu.lua. The menu is on the TOOLTIP strata, and closes on any row, on a mouse press off it (GLOBAL_MOUSE_DOWN, registered only while it is shown), and when the tray hides. Names in the pane are |Hplayer:...|h links with hyperlinks enabled; every other link in a whisper (an item, a quest) is handed to SetItemRef, so its tooltip opens. A plain left-click on a name focuses the pane's own reply box rather than starting a whisper in the main chat box. C_PartyInfo, GetCursorPosition, SetItemRef and IsModifiedClick are in .luarc.json.
  • Why it is FGI's menu and not the game's. The first build handed a right-click to SetItemRef so the game's own "FRIEND" menu opened, FGI's submenu included. In the client that menu opened behind the tray: both sit on FULLSCREEN_DIALOG (the tray's default window layer), and the tray is toplevel. Two fixes to the tray's level and strata did not hold, and the operator settled it: "you can recreate your own with the same functionality ... and then you don't have to worry about taint or any of the other stuff". Nothing of Blizzard's menu system is opened or touched now, and the strata-dropping code is gone. Every entry runs from the row's own click handler. C_FriendList.AddFriend is marked HasRestrictions in the Classic Era documentation (FriendListDocumentation.lua:13), a flag the docs do not define; whether it refuses a call from an addon's click has not been checked in a client.
  • At the top of the screen: when there is no room above the tray for the tabs and the pane, both open below it instead (Peer Review, thread 5bda8599). A flip rather than SetClampedToScreen, which would push the pane down over the tray. The decision is remade on every refresh and on every drag or resize of the tray, through a hooksecurefunc on the tray's own StopMovingOrSizing.
  • The secret-value example now tests the guard. It asserted "nothing was recorded", which stayed green with the guard bypassed: the harness's secret is a table, so WT:Record's own type() check dropped it, while in the client a secret string reports as a string. It now asserts the handler never reaches WT:Record. Both lockdown examples were driven red by bypassing the guard, then green with it restored.

Tests/zz_whisper_tabs_spec.lua covers the tabs and the menu; the example that expected a right-clicked name to reach SetItemRef was removed with that behaviour. Confirmed in a client on 2026-09-24, arrows, flash and reordering included (operator: "the whisper tab is workign how i want it to"). FGI's own right-click menu was confirmed in a client on 2026-09-25 (operator: "right click works"). The compact tray's "i" help tooltip gained a Whisper tabs paragraph (tabs, arrows, x, and the right-click menu's entries); its height hint went from 400 to 480.

Changed — LibAceGUIWidgets is a hard dependency, and Libs/GUI.lua is deleted

All seven TOCs list LibAceGUIWidgets in ## Dependencies, and .pkgmeta lists libaceguiwidgets under required-dependencies. Nothing a player loads changes: GuildRoster already hard-depends on it, so it always loaded ahead of FGI.

That load order is also why Libs/GUI.lua has not run in a client since the library moved its widgets to version 30 (when that happened was not checked). The file registered ClearFrame, GroupFrame and TLabel at version 26 and returned early if a newer copy existed. The library now registers the same three names at version 30, so the legacy window, the dump and debug windows and the intro labels have been the library's widgets. An August audit recorded GroupFrame and TLabel TIED at 26, and a tie goes to whichever addon loaded first, so FGI's copies may have won before then. Only the offline suite built FGI's copies, because Tests/support/addon.lua never loaded the library. The suite now loads it first, as the client does, and the file and its TOC line are gone. Six code comments that credited Libs/GUI.lua with Ace3's stock Frame behaviour now name the real source.

UI.CreateMenuInfo now calls the library's identical copy. The 13 places that still built menu rows with UIDropDownMenu_CreateInfo() directly now use it too (Peer Review L7). Eleven of them set checked, so they pass true and keep the check-mark slot, which gives the same call as before. The other two are the Yes/Cancel rows of the legacy window's clear-confirm menu, which are actions, not choices. They now drop the unused slot, as every other FGI action menu does. That is a few pixels of left padding on a two-row menu in the legacy window only, so it stays out of the player notes. The legacy window's resize fix below does belong in them, under Fixed. The other three helpers the library also has stay FGI's own, because each would change what players see:

  • AttachTooltip: the library's version replaces an earlier tooltip rather than adding to it, and it skips FGI's non-Latin re-font.
  • ApplyMinResize: the library's follows a UI scale that any addon in the session can set.
  • MakeLabel: the library's takes the shared accent colour instead of FGI's orange.

Every list in FGI now uses LibAceGUIWidgets' W.RowList when the installed library is MINOR 34 or later. W.RowList was extracted from FGI's list. The library added the four features FGI's tabs needed, at FGI's request: external sort, expander cells, button cells and read-only rows. addon.RowList:New hands the list to the library and translates FGI's options on the way, so no tab changed:

  • action.hidden becomes action.show.
  • The class colouring still reads NoLocaleClass through addon.ClassDisplay / addon.color, as a format function.
  • The LibLocaleOverride re-font goes through the list's own refontHook, never the library's session-global config.

Below MINOR 34, FGI's own implementation still runs, so a player with an older library loses nothing. Delete it once MINOR 34 has been released long enough. FGI's suite against the library found one library defect: a button cell whose tip answered nil still showed a header-only tooltip. The library fixed it the same hour.

Visible differences from FGI's own list: a slimmer scrollbar; blank cells sort LAST in both directions (FGI's own list sorted them first when ascending, against its own comment); number-like text sorts by value; and rows that tie keep their order. The main window's tabs were checked in a client on 2026-09-24 and look right.

Tooltips the list draws (headers, action icons, button cells) anchor through addon.Tooltip.Owner, passed as the list's own tooltipOwner. The library added that option in MINOR 34 at FGI's request, after Peer Review found the first cut anchoring them through the library. That skipped FGI's non-Latin tooltip re-font. zz_rowlist_scroll_spec checks it on both builds, and fails on the library build without the option.

Changed — LibDBIcon-1.0 is a required dependency, not an embedded copy (all but WoW Forever)

The operator's decision, delivered by TOGTools (inbox contract 08364b4b): an embedded LibDBIcon only updates for players of the addon that ships it, so every addon with a minimap button would need a re-release for each LibDBIcon update. The six non-Forever TOCs now list LibDBIcon-1.0 in ## Dependencies and no longer load Libs\LibDataBroker-1.1 or Libs\LibDBIcon-1.0; .pkgmeta lists libdbicon-1-0 under required-dependencies. The standalone loads LibStub, CallbackHandler and LibDataBroker-1.1 through its own embeds.xml, so the one dependency covers both libraries.

FastGuildInvite_Camelot.toc keeps the embedded copies, with a comment saying why: the standalone's TOC lists 11508, 11509, 20505, 20506, 30405, 38001, 40402, 50503, 50504, 120100, 120005, 120007 and not WoW Forever's 16001 (read in LibDBIcon-1.0/LibDBIcon-1.0.toc:1). The library files stay in the repo for that one TOC. No Lua changed: LibStub("LibDBIcon-1.0") and LibStub("LibDataBroker-1.1") resolve the same either way.

Tests/zz_toc_manifest_spec.lua now allows the two embedded lines on _Camelot only, and checks that the other six declare the dependency and _Camelot does not. The old spec went red on the change ("FastGuildInvite_Camelot.toc and FastGuildInvite.toc list different files") before it was updated. KNOWN COST: a player who installs by hand must now install LibDBIcon-1.0 too. Not verified in a client.

Fixed — the legacy window resized only once per time it was opened

GUI/LegacyMainWindow.lua replaced the resize grips' OnMouseUp with its own position saver. On the library's ClearFrame, that OnMouseUp is the only thing that ends a resize, so the resize state stayed set after the first drag and every later drag was refused until the window was hidden. The saver is now hooked instead (HookScript). The right-edge grip is also dropped through the library's resize handle instead of being hidden directly, because the handle re-shows every grip it owns on a UI scale change. That call runs before FGI's width lock, since it re-applies the handle's own bounds. Tests/zzzz_legacy_window_spec.lua drives two drags in a row. It fails against the old code with "the latch is stuck" and passes with the fix.

The same handle also re-applied its own bounds on any library UI-scale change: scaled, and with no maximum. That unlocked the fixed width, and at scale 2.0 it widened the window to an 800 floor (Peer Review M1). Giving the handle the 635 width would not help, because it scales whatever it holds and this window's contents do not scale. So the handle's scale listener is removed for this one window. ClearFrame's own chrome re-layout is a separate listener and still runs. The spec now changes the scale to 2.0 and checks that the width lock and the width survive.

Fixed — the Dump window's text box hung out of the bottom, and the pop-out windows were see-through

Seen in a client on 2026-09-24. Modules/dump.lua sized its text box by line count (SetNumLines(39)) with only a top anchor. That is taller than the window, so the box ran past the bottom edge and covered the status bar and Close button that LibAceGUIWidgets' ClearFrame draws there. It is now held by two opposite corners: top-left against the button column, and bottom-right 45 pixels above the window's bottom edge. That also replaces its fixed width. The window can be resized and login restores a saved width, so a narrower window used to push the box into the buttons (Peer Review). The anchors are set after every call that re-runs the box's own sizing. zzzz_dump_debug_windows_spec checks both corners.

The dump, debug and legacy windows also never received the window-opacity fill that makes the main window solid, so their backdrop showed the world through it. fn:applyWindowOpacity now reaches all three, and each applies the fill from its own OnShow. None of them is ever released, so, as with the intro pop-up, no shared-pool cleanup is owed. Both fixes were confirmed on the Dump window in a client the same day.

Fixed — the TOC author was misspelled

All seven TOCs said ## Author: Pmptasty. They now say Pimptasty, the account these addons publish under. Reported by the BijouRR peer review.

Changed — FastGuildInvite_Mainline.toc drops the stale 110207

Retail is on 12.0.5 (120005); 11.2.7 is a number no live client runs. The new TOC spec below flagged it because Ace3 no longer lists it.

Changed — GUI/Tabs/GuildRoster.lua and GUI/Tabs/Scan.lua read addon.LocNum, not the global FGI.LocNum

Each file takes local addon = FGI (GuildRoster :18, Scan :32) and then used the global -- four sites and twelve respectively. Identical in the client; offline, a spec that swaps _G.FGI for a fixture left the live tab reading the fixture, and the roster callback died on LocNum nil.

Tests — --order shuffle:12345 went from 1687 failures to 0, then a new permutation found 24 more

  • The harness's securecallfunction propagated errors (the client hands them to the error handler and returns). One raising CallbackHandler handler therefore left LibGuildRoster's registry.recurse at 1 for the rest of the run, and every later RegisterCallback was parked in insertQueue forever -- measured through a new failure message in zz_roster_rank_refresh_spec (recurse=1, parked=2). Filed to WoWAPITesting and fixed there as 8e2e404; the pin moved to it. That one root was behind the designate-election, policy-names, guild-leave, blacklist-sweep and rank-refresh clusters.

  • The trigger was ours: seven non-zz_ specs replaced _G.C_Timer and ten replaced _G.time at file scope and never put them back. policy_sync_spec's recorder had no NewTimer, so the real bridge's roster-ready handler raised in DeltaSync's P2PSession.lua:308. Every one now saves the global and restores it in a file-scope teardown; that recorder gained NewTimer. A stub time also ignores the table form time({...}), which is how the Statistics tab came to bucket against a constant.

  • Tests/support/addon.lua's load snapshot now also restores time, date, GetServerTime and print, and the fields of FGI, FGI.API, FGI.functions and C_Timer as they were at load (deleting nothing, and never FGI.DB, which initDB owns). It puts FGI's own SlashCmdList entries back into whichever table is live, since wow.reset() installs a fresh one. Measured: without the clock names, 171 failures; without print, 167.

  • announce_soundboard_spec now declares LibGuildRoster ready itself; its profiles post to guild chat, which Announce holds until the roster is built.

  • 2026-09-24, from 240 failures to 21. Two causes:

    • zz_reset_settings_spec wrote history.invites[1] into the live list and then set it to nil. When an earlier file had already logged invites, that left a hole at the front of a longer list, and every later history.trim died on invites[1].time. About 230 examples in later files failed as a result. The example now uses its own table and puts the original back. Peer Review then found the put-back was inline, after seven assertions, so any failure skipped it (M2). Every example in the file now registers its cleanup before writing, and after_each runs it. totals.accept is restored to its earlier value, not reset to 0. The "Kept-Testrealm" blacklist entry another example wrote, and never removed, is cleaned up the same way.
    • Modules/compactFrame.lua read the global FGI.LocNum at six sites, the same slip fixed above in the Guild Roster and Scan tabs. It now reads addon.LocNum. Peer Review then pointed at the rest of the class, and the other nine files that still read the global were converted too: GUI/MainWindow.lua and the Announce, Anti-Spam, Blacklist, Eligibility, Filters, History, Messages and Statistics tabs. Every one opens with local addon = FGI, so nothing changes in the client. No shipped call site reads FGI.LocNum any more.

    The 21 that remain do not come from either fix.

  • zzzz_perf_spec's one bare assert.has_error pins its message (Peer Review, 2026-09-23): a bare one accepts any error.

  • zz_scan_engine_spec declares the default subdivision axes per example and puts them back: a dozen specs write subdivideLvl, several leave it false, and every range then cost 1.

  • zz_scan_batch_spec closes the main window before each example as well as after (a window an earlier file left open made MW:Open("scan") a no-op, so the strip still read Blacklist (0)), and its failure message now lists every strip caption present.

  • 2026-09-25: seed 12345 is at 0 failures (3638 passed), and so is the default order. Removing one whisper-tab example moved the permutation again and surfaced four failures in three files, each an example relying on state no earlier file had touched. zzzz_core_spec re-arms the one-shot entering-world keybind frame (found by its handler's keybindWorldFrame upvalue) instead of assuming nobody fired the event first. zzzz_feature_detect_fallbacks_spec takes FGI.RealmResolver out of the way, since an earlier file had taught it Bob-Testrealm. zz_scan_guilded_column_spec gives the zone gate an empty set, since another spec had left Orgrimmar on the Quiet Zones list; its failure message now names which gate refused the row. Two leftover writ-cannot: markers (in those last two files) were removed. Other seeds were not run. Peer Review's count of 60+ indented _G.<name> = replacements never checked for a restore, and ~20 inline print swaps, still stands; the loader snapshot masks that class rather than surfacing it.

Tests — harness pin b818c74 → 8e2e404 → de64643

8e2e404 is the securecallfunction fix above. de64643 adds tools/verify-toc-deps.lua; its Adoption entry's own command fails from a consumer root (require("tools.toc") resolves against the working directory), which was reported back. Run from the harness root it reports four gaps for FastGuildInvite_Mainline.toc: 120005/120007/120100 missing from VersionCheck-1.0, GuildRoster, AceCommQueue-1.0 and DeltaSync. Each of the four was sent a finding; none are FGI's files.

Tests — zz_toc_manifest_spec.lua (new): the seven TOCs load the same files

Only _Mainline and _Camelot may add LibWho_Retail.lua. For one afternoon it also checked our hard dependencies' interface numbers (after Questbook found Ace3 missing 16001 for WoW Forever); that half was removed in favour of the harness's verify-toc-deps, which reads every one of a dependency's TOCs and is deliberately discovery rather than a gate. zz_presence_spec also gains _Camelot, which it had missed.

No player-visible change in this section, so docs/Curseforge_Description.html and Modules/intro.lua get no v2.14.3 entry for it.


Older releases have been moved to CHANGELOG_ARCHIVE.md — v2.14.2 and earlier, in full. v2.13.0 went section by section while it was the only version in this file (ten on 2026-09-09, twelve on 2026-09-10, eight on 2026-09-11, four on 2026-09-13, four on 2026-09-14, ten on 2026-09-16), and its heading and last section followed on 2026-09-16. v2.13.1 moved whole on 2026-09-16, v2.13.2, v2.13.3 and v2.13.4 whole on 2026-09-20, v2.13.5 whole on 2026-09-25, v2.14.0 and v2.14.1 whole the same day, v2.14.2 whole on 2026-10-03. The next candidate is v2.14.3, whole.