Commit Graph
1631 Commits
Author SHA1 Message Date
JonKazama-Hellion 293d56bdbe refactor(sidebar): move popout and greeted toggles onto the icon-button widget
Both were hand-rolled: an InvisibleButton, then a glyph pushed through the
FontAwesome handle onto the draw list, with a frozen 8px vertical offset and a
4px inset. IconButton does the hit area, the hover fill and the centred glyph,
and centres against the measured line height instead of the frozen offset.

Their order is unchanged. Both still sit after TabContextMenu.Draw, which is
the ordering the popup trigger depends on, and the caller keeps what actually
differs between them: cursor placement (SameLine vs. absolute), the visibility
rule, the glyph choice and the greeted glyph counter that
SidebarGreetedGlyphStep pins.

The popout glyph now follows the row surface rather than rowHovered ||
popHovered. That pair needed the button's own hover state one line before it
existed, and the button sits inside the row anyway, so the row's hover covers
both cases.
2026-08-17 23:52:07 +02:00
JonKazama-Hellion eacefb355d feat(sidebar): draw row surfaces, accent bar and separators
The sidebar drew a hover sweep, an icon and a label per row and nothing else.
No hover fill, no active fill, no accent bar, no separator -- the active tab
was visually indistinguishable from the rest. The theme preview in settings has
been showing all of it for months without the real sidebar delivering any.

Rows now go through the Row widget: base surface, active surface, hover
interpolated between them, a 2px accent bar on the active row and a bottom
separator.

Hover is detected with IsMouseHoveringRect, not IsItemHovered. The row button
is up to two hit widths narrower than the row itself (popout slot, greeted
slot), so a full-width surface driven by the item would flicker at the edges.
AllowWhenBlockedByActiveItem is required on the window check, otherwise the
fill disappears the moment the button is pressed, because InvisibleButton owns
the active id by then.

ItemSpacing is pushed to zero around the row loop so surfaces sit flush instead
of leaving a stripe of window background between them. Style vars are a global
stack and the context menu inherits them, so TabContextMenu now restores a
normal spacing inside its popup -- without that its entries would touch.

The row surface is drawn strictly with draw-list calls between the row button
and TabContextMenu.Draw, which is the ordering the popup trigger depends on.
2026-08-17 23:50:52 +02:00
JonKazama-Hellion 464bd52887 refactor(sidebar): move section headers onto the divider widget
Separator plus TextDisabled took all of their vertical breathing room from
ItemSpacing. The next task pushes ItemSpacing to zero so rows sit flush, which
would have collapsed the header onto its neighbours.

LineDivider carries its own padding and submits its own layout item, so it no
longer depends on the surrounding spacing at all. The compact branch keeps
suppressing only the caption, and LastDrawnSectionHeaderCount still increments
exactly where the caption is drawn -- SidebarSectionHeaderStep pins it at 2
with compact off and 0 with compact on.
2026-08-17 23:49:15 +02:00
JonKazama-Hellion 1e8a60ac80 fix(sidebar): scale the drawn width without moving the stored value
The sidebar constants were raw pixels. At 150% display scaling the text grows,
the column does not, and the row contents stop fitting.

GetWidth and IsExpanded stay unscaled on purpose. The stored width and the
switch threshold are user settings in design pixels, and
SidebarModeAutoSwitchStep compares GetWidth's return value against the raw
bounds with exact equality -- scaling there would fail the step at anything
other than 100%. Scaling happens once, at the single draw call site.

RowHeight and the two hit widths now come from Metrics. The row internals read
GetContentRegionAvail, so they follow automatically and the hit-area split
thresholds stay proportional.

The not-ready branch is scaled too: a scale change triggers a font rebuild, so
that branch really is hit while GlobalScale is moving, and an unscaled width
there makes the sidebar jump.

The width slider referenced the bounds as literals. It now uses the constants,
so it cannot drift away from the clamp.

Known remainder, deliberate: SidebarAutoSwitchThresholdPx is compared against
real screen pixels while the columns now scale, so the switch point drifts at
high scaling. Scaling it would fail the same SelfTest. Noted for v1.11.0.
2026-08-17 23:48:36 +02:00
JonKazama-Hellion 10b345821f fix(messages): apply density switches immediately, freeze the cache while waiting
Review of block A found two regressions the settle gate introduced.

Density and the two name modes are switches, not sliders. They land on a new
value in one frame and stay there, but the gate made them wait 200ms like a
drag. Meanwhile the row painter switched instantly, so for about twelve frames
the planner ran compact rows against card heights: wrong slice, wrong offset,
and the visible rows got overwritten with the new density while everything
else kept the old one. Before the gate this was correct, so it was a
regression, not a pre-existing bug. The fingerprint now separates a discrete
axis that applies at once from the continuous one that waits.

Second, the cache did not actually stand still during the wait. Measurements
were written back unconditionally, so a resize drag mixed heights from many
different widths and the lead dummy drifted for the whole drag. The gate now
exposes IsPending and the planned path skips the write while it is set. The
linear path still measures, because without a filled cache there is nothing to
plan against.

Three smaller items from the same review:

An oscillation slower than the settle window used to bypass the wait entirely,
because the pending clock was never reset on the way back to the applied value.

A value that changes on every frame could hold the gate shut forever while the
applied fingerprint stayed wrong. MaxWaitMs is the deadline, checked before the
still-moving branch so a permanently moving value actually reaches it.

The row painter was passed as `compact ? DrawCompactRow : DrawCardRow`. Both
are instance methods, so the method group captures `this` and Roslyn does not
cache it: 64 bytes per frame per window. Bound once in the constructor now. The
per-frame height array is also reused rather than reallocated, which at the
default MaxLinesToRender was 10 KB per frame on what A2 had just made the
default path.
2026-08-17 23:46:43 +02:00
JonKazama-Hellion 460217459c chore(style): add a widget gallery for visual verification
Every widget in its states, reachable via /hellion widgets. The point is to
check them one at a time before they land in real components: the v2.x style
engine grew three primitives that were never wired to a call site
(DrawGlowBorder, DrawSlipPolygon, DrawHonorificHeader), and this is the cheap
way to notice that before a cycle closes.

DEBUG-only, like SeStringDebugger. It is a verification aid, not a feature, so
it never reaches a release build -- verified against a Release compile.

The header line also shows the live GlobalScale and the hover registry size,
which makes both the scaling work and the eviction contract observable while
dragging the Dalamud scale slider.
2026-08-17 23:42:05 +02:00
JonKazama-Hellion d6ad81e8ca feat(style): add the drawn-widget set
Five widgets, each with a real consumer inside this cycle: Row for sidebar
rows, Badge for unread counts, IconButton for the popout and greeted toggles,
LineDivider for section headers, Pill for the channel pill and the status bar.

Row carries no Tab on purpose. v1.11.0 moves the sidebar from tab rows to
channel rows, and if the chrome sits in a Tab-free widget that cycle only has
to change the caller.

IconButton is deliberately small. Its two sidebar call sites differ in
placement, visibility rule, glyph choice, and one of them bumps a SelfTest
counter. Taking all of that as parameters would produce a widget that is five
switches and no behaviour, so the caller keeps them.

LineDivider brings its own vertical padding and submits its own layout item.
The sidebar is about to push ItemSpacing to zero so rows sit flush, and a
divider relying on spacing would collapse onto its neighbours there.

Colours go through WidgetPalette. TokenResolver returns RGBA and ImDrawList
expects ABGR; with five widgets that is the same swap-red-and-blue trap five
times over.

Sizes go through WidgetGeometry, which is pure so the build suite can pin it,
and clamps every result to a positive extent -- ImGui asserts on a zero-sized
InvisibleButton and takes the window with it.
2026-08-17 23:40:15 +02:00
JonKazama-Hellion 31fa410875 fix(style): drive the hover sheen from held state, drop the leaking start map
The sheen kept its own Dictionary<string, DateTime> of start timestamps and
only cleared an entry in the un-hover branch. A row that disappeared while the
pointer was on it left its entry behind until the plugin reloaded, which is
exactly what happens to temp tabs under the LRU limit.

It now takes the held intensity from HoverState and draws on the rising edge
only. The alpha falls off as the value climbs, so the sweep has faded out by
the time the surface underneath is fully in. On the way out it simply does not
run, which is what stops it from travelling backwards -- the old
SheenStarts.Remove prevented that by resetting, and dropping the map without
this guard would have reintroduced it.

The sidebar call site built "sidebar.tab.{guid}" per row per frame, two
allocations each. Master spec 5.3 asks for constant keys, and 7.5 for a stable
allocation count. It now uses ImGui.GetID("row"u8), which is allocation-free
and seeded from the window's ID stack, so the same literal stays distinct per
window and per PushID'd tab.

HoverSheenAllocStep pinned the old dictionary contract and is replaced by
HoverStateFootprintStep, which pins the same property against the registry:
repeated queries add no entries, and an element that stops being queried leaves
the map instead of leaking.
2026-08-17 23:37:24 +02:00
JonKazama-Hellion bf0c3e2bd7 feat(style): hold hover state across frames instead of one-shot sweeps
DrawHoverSheen measured its own elapsed time against DateTime.UtcNow and gave
up after 0.65s, so a row stopped reacting while the pointer was still on it.
There was no held value to interpolate colours against.

HoverState keeps one 0..1 intensity per element, rising at 14/s and falling at
8/s. Slower out than in is what makes the fade read as deliberate.

Query and advance are separate on purpose. Several SelfTest steps call
Sidebar.Draw against the live tab list, so the same element gets submitted up
to three times in one frame, twice from a window the mouse is not over. If the
query advanced the value, the last caller would win and the fade would run
backwards. Query only ORs the hover flag; BeginFrame does all the moving and
the eviction.

BeginFrame sits above the HideInLoadingScreens and New Game+ early returns, so
a hidden main window still lets pop-out hovers fade out instead of freezing
mid-blend.

FrameLerp gains Ramp: Smooth approaches asymptotically and never arrives, so a
value driven by it would never reach zero and never become evictable.

ReduceMotion short-circuits before the map is touched, returning a hard 0 or 1.
An infinite rate would produce NaN and poison the entry for the session.
2026-08-17 23:35:54 +02:00
JonKazama-Hellion 28b97b3f70 feat(style): add a scale-aware metrics layer
Layout constants lived as bare floats in the components: row heights, hit
widths, insets, reserve widths. None of them multiplied by GlobalScale, so at
125% or 150% display scaling the text grows and the boxes do not. The quick
buttons stop fitting into their 130px column.

Metrics holds the design values and exposes scaled properties. The Raw
constants stay reachable for the few places that must store or compare an
unscaled value: Sidebar.GetWidth (a SelfTest pins it against raw numbers) and
the width slider bounds.

Scale is pinned once per frame against ImGui.GetFrameCount(). GlobalScale is a
live value the Dalamud slider moves on every dragged frame, so reading it per
access can shift a CalcSize away from its matching Draw inside one frame, and
costs three native calls each time.

Not in ThemeLayout: that record is serialised into theme JSON, and layout
customisation is out of scope per the master spec.

MetricsMath is the pure half so the build suite can pin the arithmetic. Text
heights are measured rather than scaled -- the font is built from
Config.FontSizeV2, which GlobalScale does not feed into.
2026-08-17 23:34:08 +02:00
JonKazama-Hellion 0ef33934a2 refactor(messages): plan compact rows like cards instead of assuming a fixed height
Compact mode ran an ImGuiListClipper with CompactRowHeight = 18f. Two things
were wrong with that.

The number: at the default 12.75pt the font is 17px, and ChunkRenderer pushes
ItemSpacing to zero for the whole chunk loop, so a single-line compact row
advances the cursor by 17, not 18. The clipper seeded the cursor one pixel too
low per row, which accumulates into a visible drift against the scrollbar.

The assumption: compact rows are not constant height at all. DrawCompactRow
renders content with wrap: true, and WrapEncodedLine submits one text item per
wrapped line. At 620px and 17px type that kicks in around 70 characters, so
most chat lines are multi-line.

Both densities now share DrawRows/DrawLinearAndMeasure over the existing
CardClipPlanner, with the row painter passed in. The fixed height and the
clipper are gone.

Also corrects the CompensatedDummy comment: the cached heights carry no
trailing ItemSpacing (every row ends inside DrawChunks, where spacing is
zero, and ImGui writes the advance at submission). The compensation is
correct because it cancels the spacing the dummy itself appends.

CardClipPlanStep drives the invalidation hook directly, which now sits behind
the settle gate, so it walks a synthetic clock past the window.
2026-08-17 23:32:04 +02:00
JonKazama-Hellion 39a8e95581 fix(messages): invalidate the height cache when UI scale changes
The layout fingerprint tracked font size, density, both name modes and
content width, but not ImGuiHelpers.GlobalScale. Scale feeds
CalcWordWrapPositionA, so changing it rewraps every row while the cached
heights stay put and the clipper dummies drift against the scrollbar.

Two further problems came out of the same code:

The fingerprint lived in a single field on MessageList while the cache it
guards is per tab. Resizing in tab A marked the new value applied, so tab B
kept measuring against the old width. It is now a gate per tab identifier.

Acting on every fingerprint change is too eager. A window resize or a drag on
the Dalamud UI-scale slider moves the value on every frame, and each change
drops the cache and forces the linear measure path over the whole tab (up to
Config.MaxLinesToRender rows). The gate now waits for the value to settle for
200ms, which turns a drag into one rebuild instead of one per frame.

The settle logic sits in Util/LayoutFingerprint.cs as a plain value type so
the build suite can pin it without standing up an ImGui frame.
2026-08-17 21:17:38 +02:00
JonKazama-Hellion 3583dfc032 perf(tells): build the tab outside the lock, guard pin transitions
HandleTell was one atomic block, and PreloadHistory sat inside it -- so every
new tell partner held TabsListLock across a store query that sorted the whole
receiver history before returning a row. That is the lock the draw thread and
the message worker both wait on.

Now three steps: look for an existing tab under the lock, build the new one
(including history) without it, then commit under the lock again. Splitting it
opens a window where the world can change, so the second block re-checks:

- FindTempTab again, in case something else created the tab meanwhile. The
  message goes to that one instead. Not in the first block's early return --
  HandleTell runs after the delivery loop, so an existing tab already has it and
  adding again would duplicate the line.
- A generation counter, bumped by OnLogout under the same lock. A logout in
  between wipes the unpinned pool, and without this the freshly built tab would
  outlive it and show up for a character we already left. Not via
  CurrentContentId: its getter falls back to a cached value, so the comparison
  can silently pass.
- The pool cap moves into CommitTempTab and stays there exactly once. Evaluating
  it twice would evict a tab on every spawn.

Pin, unpin and promote take the lock around the flag change now -- they decide
pool membership and whether a save strips the tab. SaveConfig stays outside, so
no fsync lands on the click path.

DropOldestTempTab removes by reference: the index came from an earlier Select in
the same block and would point at the wrong tab if anything shifted the list.
2026-08-17 18:38:08 +02:00
JonKazama-Hellion c34024a18b chore(release): bump assembly version to 1.9.0
The whole v1.9.0 cycle ran without a version bump, so the plugin still reported
1.8.8 in-game. For a tester beta that is untenable: bug reports would name a
version that does not identify the code they ran.

Download links stay pinned to v1.5.6 on purpose. There is no v1.9.0 release, and
pointing at an artifact that does not exist is worse than the visible mismatch
between assembly version and download target. Public stays on v1.5.6 until
v2.0.0.
2026-08-17 07:27:59 +02:00
JonKazama-Hellion eaed0b13e0 fix(config): guard the shared config maps, restore lost fields in Tab.Clone
TabsListLock never covered ChatColours, PrivacyPersistChannels or
RetentionPerChannelDays, yet the settings UI mutates them from the draw thread
while the retention thread can be serializing the same config. Adding a new key
to a dictionary or a new element to a set invalidates a running enumeration, so
this could throw from inside JsonConvert.SerializeObject.

Not a corner case: the colour picker lists 66 channels but only 25 ship with a
default, so the first edit of any of the remaining ones inserts a new key -- and
the reset button removes a key, which makes the next edit a fresh insert again.

The readers matter as much as the writers. IsAllowedForStorage runs per message
on the worker thread and asks PrivacyPersistChannels whether a channel may be
stored; a Contains racing an Add that resizes buckets can answer wrong, and that
answer decides whether a message is written to disk. The retention sweep
enumerates RetentionPerChannelDays on the framework thread while the wizard can
clear it -- Clear does not throw there, it just cuts the enumeration short, so
the sweep would run on half a policy.

New ConfigMapsLock covers all of it. It sits inside TabsListLock (that edge is
real, AutoTellTabsService calls SaveConfig while holding the tabs lock), never
the other way round -- so every call site closes the lock before saving.

Tab.Clone silently dropped Icon and ChatCodes, both serialized. A reflection
test now walks the serialized fields so a future one cannot slip past.

Also: CurrentTab read Count and [0] as two separate accesses.
2026-08-17 07:27:48 +02:00
JonKazama-Hellion 2b4243599e fix(ui): render each frame from one tab-list snapshot, key widgets by identity
Sidebar, top tabs and status bar each read Config.Tabs on their own, unlocked,
while the worker added or evicted tabs. That gave three independent views of a
moving list: an index built in one place could resolve to a different tab a few
lines later, which showed up either as an out-of-range crash on the draw thread
or -- worse, because it is silent -- as a click landing in someone else's tell.

MainWindow now takes one snapshot under the lock and passes it through the whole
frame. Deliberately a shallow copy: tab identity is compared by reference all
over the draw path, so cloning would break every ReferenceEquals and Contains.
ChangeTabDelta and ResetActiveTabIfRemoved run on the framework thread and keep
their own locked reads instead; ThemeQuickPicker locks its own copy, since
reaching it would mean threading a parameter through InputBar, which popouts
share and which has no tab list.

Widget IDs move from list position to tab.Identifier. ImGui carries popup and
widget state across frames under that ID, so a position-based one re-binds an
open context menu to a different tab as soon as the list shifts -- a snapshot
cannot fix that, it spans frames. This also resolves top tabs visually merging
into each other when the list changed.

The sidebar section headers counted over the live list while the rows came from
BuildRenderOrder, which skips popped-out tabs. Both sides take the same
predicate now, so the count matches what is drawn.
2026-08-17 07:27:34 +02:00
JonKazama-Hellion 24dff3cc2e fix(messages): snapshot the tab list before delivering a message
ProcessMessage walked Config.Tabs live on the worker thread while SaveConfig's
strip and the auto-tell spawn mutated the same list under TabsListLock. The
resulting "collection was modified" was caught by the pending-message handler
and only logged -- so the message was dropped entirely: no tab entry, no sound,
and MessageProcessed never fired, which also meant no tell tab and no routing.
Silent message loss, exactly under the load where it hurts.

The loop now runs over a snapshot taken under the lock. AddMessage stays
outside it, so the lock order (list outer, MessageList inner) is unchanged.

SelectNotificationSound reports which tab it picked, so playback can skip a tab
that disappeared between snapshot and sound -- otherwise the snapshot would let
an evicted tab still make noise.

While here: the current tab was read twice despite the comment claiming it was
snapshotted once.
2026-08-17 07:27:21 +02:00
JonKazama-Hellion 89c66e0d3d perf(store): index (Receiver, Date) so tell history streams sorted
GetTellHistoryWithSender filters on Receiver and orders by Date DESC. Without a
matching index SQLite sorted the whole receiver history into a temp b-tree
before yielding row one -- measured 10 ms to first row against 9621 tells, all
of it under TabsListLock, which defeats the early break in the caller.

(Receiver, ChatType, Date) does not help: the ChatType IN filter sits between
the equality prefix and the sort column, so the temp b-tree stays. Verified on
a real database: plan now reads SEARCH ... USING INDEX idx_messages_receiver_date
with no sort step.

No SQL LIMIT -- an earlier 500-row cap was removed in v1.4.10 because it cut
less-frequent partners off the back of the window, and 83% of partners have
fewer than 21 tells in total.

The migration dispatcher is cumulative, so Migrate5 is appended to every
existing case, not just the new one.
2026-08-17 06:50:04 +02:00
JonKazama-Hellion d2da51a4f7 perf(settings): write config on release instead of every slider frame
ImGui sliders report a change in every frame the value moves, so dragging one
rewrote the full 31 KB config to disk per frame -- serialize, fsync and rename,
synchronously on the draw thread. Measured on Linux/Wine that showed up as a
114 ms frame while the plugin itself only drew for 2.9 ms; the rest was waiting
on the write.

The five shared slider helpers now defer SaveConfig to IsItemDeactivatedAfterEdit,
matching what ChatColourPicker already did for the colour wheel.

Renaming a tab needed its own path: the input lives inside a popup, and ImGui
never re-submits it when the popup is dismissed by clicking outside, so
IsItemDeactivatedAfterEdit would not fire and the new name would be lost. A
pending-rename marker scoped to the owning tab flushes it when the popup is
gone -- scoped, because every other tab's Draw reaches that branch too.

DeferredSaveFrames is removed: the debounce was fully wired but never armed,
and this approach makes it redundant.
2026-08-17 06:49:53 +02:00
JonKazama-Hellion 99dca8cb31 fix(selftests): take TabsListLock around Config.Tabs mutations
The three sidebar/coupling steps add, insert and remove tabs straight from
the framework thread while the message worker mutates the same list under
TabsListLock. CurrentTabCouplingStep's Insert(0, ...) is the worst of them:
it shifts every index, so DropOldestTempTab can remove the wrong tab between
its index lookup and RemoveAt.

Locks sit around the individual mutations, never around a Draw call, so no
step holds the lock across rendering.
2026-08-17 06:49:42 +02:00
JonKazama-Hellion ce5973aea9 Merge remote-tracking branch 'origin/main' into feature/v1.9.0 2026-08-16 21:08:20 +02:00
JonKazama-Hellion fd8e5a1a17 revert(ci): security-scan wieder ueber den reusable workflow
Security Scan (reusable) / Security Scan (push) Failing after 27s
Security / scan (push) Failing after 27s
Build / Build (Release) (push) Successful in 36s
Rueckbau des Inline-Workarounds. Der Runner konnte den reusable workflow
nicht mehr laden, weil git fetch gegen die Forge crashte. Ursache war kein
Bug, sondern eine Kompromittierung ueber CVE-2026-59774: ein injizierter
packObjectsHook ersetzte git pack-objects durch einen Malware-Dropper.

Gitea laeuft jetzt auf 1.27.2, der Hook ist entfernt, fetch funktioniert
wieder. Die Scan-Definition liegt damit wieder an einer Stelle statt in
sieben Dateien. Details: Obsidian "Projekte/Hellion Forge/Forge
Security-Incident 2026-08-15.md".
2026-08-15 21:17:43 +00:00
JonKazama-Hellion d50f2cea90 fix(ci): security-scan inline statt reusable workflow
Security / Security Scan (push) Failing after 21s
Build / Build (Release) (push) Failing after 34s
act_runner laedt reusable workflows per git-clone ueber HTTPS. Dieser Pfad
ist auf der Forge seit 2026-08-12 defekt (git upload-pack --stateless-rpc
bricht mit BUG "packfile_uris requires sideband-all" ab). Die Scans liefen
bisher nur ueber eine im Juni gecachte Kopie im Runner.

Der Scan steht jetzt vollstaendig in dieser Datei, der Quellstand kommt als
tar-Archiv statt ueber actions/checkout. Zurueckbauen, sobald git fetch
ueber HTTPS wieder funktioniert.
2026-08-15 17:26:51 +00:00
JonKazama-Hellion de9d11ba4a docs: v1.9.0 comment-pass (Z-3) — fix false TEST-MIRROR paths, comment accuracy + density 2026-06-16 20:59:09 +02:00
JonKazama-Hellion 618e029ff4 perf(tabs): share Plugin.TabsListLock across AutoTellTabsService + MessageManager refilter + SaveConfig (B3) 2026-06-16 20:39:56 +02:00
JonKazama-Hellion 93f4fbba72 perf(card): variable-height clipper + layout-fingerprint cache invalidation + clip-plan self-test 2026-06-16 20:09:45 +02:00
JonKazama-Hellion 1d69d0cc30 B2: add Dalamud-free CardClipPlanner variable-height clip-plan helper 2026-06-16 19:58:02 +02:00
JonKazama-Hellion b048a51534 B1: dedupe CJK/symbols merge via AddCjkAndSymbols, trim fallback range; FontsReady + report self-test 2026-06-16 19:53:39 +02:00
JonKazama-Hellion 7d2fd1ab65 selftest: add on-disk SelfTestReport log; report PASS/FAIL details from steps 2026-06-16 19:53:39 +02:00
JonKazama-Hellion 2c5c40524d B1: add Dalamud-free CjkFallbackRange helper + coverage tests 2026-06-16 19:25:34 +02:00
JonKazama-Hellion 0508a05bab test(selftest): add GlobalStyleScope GC-reserve alloc probe 2026-06-16 19:23:15 +02:00
JonKazama-Hellion 32013babaf perf(style): make GlobalStyleScope.StackHandle GC-free via counter scope 2026-06-16 19:23:15 +02:00
JonKazama-Hellion 430c8f235a perf(baseline): 1000-frame steady-state capture with quad-proxy draw calls + JSON sink 2026-06-16 19:13:45 +02:00
JonKazama-Hellion 68c4e28495 perf(baseline): time full Draw() handler into LastDrawMs field 2026-06-16 19:13:45 +02:00
JonKazama-Hellion 458df3b4bd Restore permanent-REPLY game-side tell pre-targeting (1.5.6 parity) 2026-06-16 19:03:34 +02:00
JonKazama-Hellion 6733c7f92e Restore pop-out exclusivity: hide popped tabs from main window + keybind/unread parity 2026-06-16 18:30:23 +02:00
JonKazama-Hellion fa20b53455 C2/C3: restore rotation keybinds (REPLY/LS-cycle) + focus contract routing on the focused chat surface 2026-06-16 14:09:28 +02:00
JonKazama-Hellion ea3f00f107 GP-04: reset Tab.PopOut on load via shared helper (clears stale pinned flags) 2026-06-16 13:26:17 +02:00
JonKazama-Hellion 5bdf4217d6 D1-3 (XC-8): route PayloadHandler Send-Tell through shared BuildTellCommand (IsPublic at call-site) 2026-06-16 13:10:22 +02:00
JonKazama-Hellion 97e58934d6 D1 (XC-8): extract shared PrefillTellInput/BuildTellCommand for the two tell-prefill detours 2026-06-16 13:03:26 +02:00
JonKazama-Hellion f05d7104af A3: gate pop-out affordance on expanded sidebar (icon-only overlap fix) 2026-06-16 12:33:26 +02:00
JonKazama-Hellion e98cefa0b2 A2: name Sheen tuning knobs + SmoothStep crossfade easing 2026-06-16 12:29:57 +02:00
JonKazama-Hellion c1765ce8ca A1: harden HoverSheenAllocStep with real dictionary-footprint assertion 2026-06-16 12:22:29 +02:00
JonKazama-Hellion 7ead120213 A1: scharfschalten DrawHoverSheen accent-tint (Variante A) 2026-06-16 12:17:39 +02:00
JonKazama-Hellion f5ba1c0246 A1: add ColourUtil.LerpTowardWhite accent-tint helper 2026-06-16 12:02:53 +02:00
JonKazama-Hellion b372ab84c3 Merge branch 'feature/v1.8.0' into main 2026-06-16 09:18:12 +02:00
JonKazama-Hellion c27785f9f6 Merge branch 'feature/v1.8.0-closeout' into feature/v1.8.0 2026-06-16 09:07:57 +02:00
JonKazama-Hellion 6f71b09331 fix(closeout): address closure-review findings
- gate keybind pill-sync on IsChannelOrExistingLinkshell so an empty
  linkshell slot no longer desyncs the pill from the real send channel
- close manually-popped pop-out windows on logout via an IsOpen filter
  instead of the PopOut flag (which manual pops never set)
- read the router's tell-tab lookup through a lock-wrapped accessor so the
  framework thread cannot enumerate Config.Tabs mid worker-thread mutation
- add a "switch on every tell" toggle (default on) and make the auto-open
  mode pick the matching layout, so Sidebar vs Top-tab are distinct
- comment corrections (stale/contradictory text, TEST-MIRROR path depth)
2026-06-16 09:04:18 +02:00
JonKazama-Hellion 49f5119b17 feat(popout): arm the auto-tell pop-out settings and add the pool self-test 2026-06-16 01:29:59 +02:00
JonKazama-Hellion 6578c10b13 feat(settings): restore the tab-cycle keybind binder UI 2026-06-16 01:21:05 +02:00