From 9dc0fcd961969db4d358776e38982f26ab649ad3 Mon Sep 17 00:00:00 2001
From: Views2k <292889387+Views2k@users.noreply.github.com>
Date: Thu, 15 Oct 2026 22:27:42 +0000
Subject: [PATCH 1/8] Keep Wisp's GPU scheduling priority at the game's level
The renderer raised the process scheduling class to HIGH and the device thread priority to +1 at every hardware device creation. That lets the overlay's presents preempt the game's GPU work on each frame, which matches a report of frame drops, stutter and short audio glitches while Wisp is on screen. The renderer now only reads the effective priorities back for diagnostics; the contract tests require that creation leaves the process class unchanged.
---
.../NativeContractTests.cpp | 29 +++++++------------
.../Wisp.NativeRenderer.cpp | 19 ++++++------
src/Wisp.NativeRenderer/Wisp.NativeRenderer.h | 4 ++-
3 files changed, 24 insertions(+), 28 deletions(-)
diff --git a/src/Wisp.NativeRenderer/NativeContractTests.cpp b/src/Wisp.NativeRenderer/NativeContractTests.cpp
index 83b0fd72..a094a881 100644
--- a/src/Wisp.NativeRenderer/NativeContractTests.cpp
+++ b/src/Wisp.NativeRenderer/NativeContractTests.cpp
@@ -266,9 +266,9 @@ static_assert(sizeof(WispWaitMetrics) == 48 && sizeof(WispWaitMetricsV2) == 72 &
static bool IsUnattempted(const WispGpuPriorityStatus& status)
{
- return status.attempted == 0 && status.requestedProcessClass == D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH &&
+ return status.attempted == 0 && status.requestedProcessClass == D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL &&
status.processSetStatus == INT32_MIN && status.processReadStatus == INT32_MIN &&
- status.effectiveProcessClass == -1 && status.requestedDevicePriority == 1 &&
+ status.effectiveProcessClass == -1 && status.requestedDevicePriority == 0 &&
status.deviceSetHResult == S_FALSE && status.deviceReadHResult == S_FALSE &&
status.effectiveDevicePriority == INT32_MIN;
}
@@ -279,21 +279,22 @@ static void CheckGpuPriority(void* renderer, bool cpuRendering,
WispGpuPriorityStatus status{};
Require(WispRendererGetGpuPriorityStatus(renderer, &status) == S_OK,
"GPU priority initialization snapshot is available on owner thread");
- Require(status.requestedProcessClass == D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH &&
- status.requestedDevicePriority == 1, "GPU priority requests HIGH and relative plus one only");
+ Require(status.requestedProcessClass == D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL &&
+ status.requestedDevicePriority == 0, "GPU priority never requests more than the defaults");
+ Require(status.processSetStatus == INT32_MIN && status.deviceSetHResult == S_FALSE,
+ "GPU priority setters are never called");
auto currentClass = D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL;
const NTSTATUS currentRead = D3DKMTGetProcessSchedulingPriorityClass(GetCurrentProcess(), ¤tClass);
+ if (beforeProcessRead >= 0 && currentRead >= 0)
+ Require(currentClass == beforeProcessClass, "renderer creation leaves the process GPU priority unchanged");
if (cpuRendering)
{
- Require(IsUnattempted(status), "WARP skips priority setters and readbacks without reporting success");
- if (beforeProcessRead >= 0 && currentRead >= 0)
- Require(currentClass == beforeProcessClass, "WARP leaves existing process GPU priority unchanged");
+ Require(IsUnattempted(status), "WARP skips priority readbacks without reporting success");
}
else
{
- Require(status.attempted == 1 && status.processSetStatus != INT32_MIN &&
- status.processReadStatus != INT32_MIN && status.deviceSetHResult != S_FALSE &&
- status.deviceReadHResult != S_FALSE, "hardware initialization records all four priority call results");
+ Require(status.attempted == 1 && status.processReadStatus != INT32_MIN &&
+ status.deviceReadHResult != S_FALSE, "hardware initialization records both priority readbacks");
if (status.processReadStatus >= 0)
{
Require(status.effectiveProcessClass >= D3DKMT_SCHEDULINGPRIORITYCLASS_IDLE &&
@@ -302,19 +303,11 @@ static void CheckGpuPriority(void* renderer, bool cpuRendering,
if (currentRead >= 0)
Require(status.effectiveProcessClass == static_cast(currentClass),
"stored process priority agrees with independent OS readback");
- if (status.processSetStatus == 0)
- Require(status.effectiveProcessClass == status.requestedProcessClass,
- "successful process priority set and readback agree");
}
else Require(status.effectiveProcessClass == -1, "failed process readback retains unknown class");
if (status.deviceReadHResult == S_OK)
- {
Require(status.effectiveDevicePriority >= -7 && status.effectiveDevicePriority <= 7,
"successful device priority readback is a relative priority");
- if (status.deviceSetHResult == S_OK)
- Require(status.effectiveDevicePriority == status.requestedDevicePriority,
- "successful device priority set and readback agree");
- }
else Require(status.effectiveDevicePriority == INT32_MIN, "failed device readback retains unknown priority");
}
std::printf("gpuPriorityAttempted=%u;processSet=0x%08lX;processRead=0x%08lX;processEffective=%d;deviceSet=0x%08lX;deviceRead=0x%08lX;deviceEffective=%d\n",
diff --git a/src/Wisp.NativeRenderer/Wisp.NativeRenderer.cpp b/src/Wisp.NativeRenderer/Wisp.NativeRenderer.cpp
index 2c968b35..a850c0bb 100644
--- a/src/Wisp.NativeRenderer/Wisp.NativeRenderer.cpp
+++ b/src/Wisp.NativeRenderer/Wisp.NativeRenderer.cpp
@@ -36,10 +36,13 @@ namespace
constexpr UINT MaximumFrameLatency = 2;
constexpr uint64_t MaximumDialCacheBytes = 16ull * 1024 * 1024;
+ // Wisp never raises its GPU scheduling priority: the game keeps the GPU, and
+ // the overlay accepts whatever time is left. The requested values are the
+ // untouched defaults; only the readbacks are attempted on hardware.
constexpr WispGpuPriorityStatus UnattemptedGpuPriorityStatus() noexcept
{
- return {0, D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH, INT32_MIN, INT32_MIN, -1,
- 1, S_FALSE, S_FALSE, INT32_MIN};
+ return {0, D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL, INT32_MIN, INT32_MIN, -1,
+ 0, S_FALSE, S_FALSE, INT32_MIN};
}
bool ValidSize(uint32_t width, uint32_t height) noexcept
@@ -667,19 +670,17 @@ namespace
pacingStatus = S_OK;
}
- void ConfigureGpuPriority(IDXGIDevice* dxgiDevice) noexcept
+ void ObserveGpuPriority(IDXGIDevice* dxgiDevice) noexcept
{
gpuPriorityStatus.attempted = 1;
- // Best-effort test: neither failure prevents rendering, and no
- // elevation, absolute priority or realtime class is requested.
- gpuPriorityStatus.processSetStatus = D3DKMTSetProcessSchedulingPriorityClass(
- GetCurrentProcess(), D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH);
+ // Read-only: a raised process class or device priority would let the
+ // overlay's presents preempt the game's GPU work on every frame. The
+ // setters are never called; the readbacks only feed diagnostics.
auto processClass = D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL;
gpuPriorityStatus.processReadStatus = D3DKMTGetProcessSchedulingPriorityClass(
GetCurrentProcess(), &processClass);
if (gpuPriorityStatus.processReadStatus >= 0)
gpuPriorityStatus.effectiveProcessClass = static_cast(processClass);
- gpuPriorityStatus.deviceSetHResult = dxgiDevice->SetGPUThreadPriority(1);
INT devicePriority = INT32_MIN;
gpuPriorityStatus.deviceReadHResult = dxgiDevice->GetGPUThreadPriority(&devicePriority);
if (gpuPriorityStatus.deviceReadHResult == S_OK)
@@ -850,7 +851,7 @@ namespace
context->ClearRenderTargetView(renderTarget.Get(), clear);
CHECK_HR(InitialPresentationResult(PresentChain(swapChain.Get(), swapChainFlags, 0)));
CHECK_HR(composition->Commit());
- if (!cpuRendering) ConfigureGpuPriority(dxgiDevice.Get());
+ if (!cpuRendering) ObserveGpuPriority(dxgiDevice.Get());
return S_OK;
}
diff --git a/src/Wisp.NativeRenderer/Wisp.NativeRenderer.h b/src/Wisp.NativeRenderer/Wisp.NativeRenderer.h
index 07cc983b..ef15e3a9 100644
--- a/src/Wisp.NativeRenderer/Wisp.NativeRenderer.h
+++ b/src/Wisp.NativeRenderer/Wisp.NativeRenderer.h
@@ -40,7 +40,9 @@ struct WispWaitMetricsV2
// Stored initialization results, not a live query. Process statuses are raw
// NTSTATUS values; INT32_MIN means not attempted. Device statuses are HRESULTs
// (S_FALSE when not attempted). Effective values stay -1/INT32_MIN if unread.
-// attempted is zero for WARP; requested values describe the hardware test only.
+// attempted is zero for WARP. The renderer never sets a priority: requested
+// values stay at the defaults (NORMAL, 0) and only the readbacks run on hardware,
+// so the set statuses always report "not attempted".
struct WispGpuPriorityStatus
{
uint32_t attempted;
From 88c679d815e9e719523ccd725908307ae0623025 Mon Sep 17 00:00:00 2001
From: Views2k <292889387+Views2k@users.noreply.github.com>
Date: Thu, 15 Oct 2026 22:58:19 +0000
Subject: [PATCH 2/8] Make the HUD's GPU priority a setting, off by default
Renderer creation no longer raises the process scheduling class; a new WispRendererSetGpuPriority export raises HIGH / +1 or restores NORMAL / 0 on the render thread, and the render worker applies the user's choice whenever it changes. The setting lives in HUD settings as "Prioritize HUD updates on the GPU", is reported in the Tools environment snapshot as hudGpuPriority, and can be paused from Feature controls as gpu-priority.
---
src/Wisp.App/AppController.FeatureControls.cs | 1 +
src/Wisp.App/AppController.cs | 22 +++++++
src/Wisp.App/AppSettings.cs | 2 +
src/Wisp.App/ControlPanelWindow.cs | 7 +++
src/Wisp.App/DiagnosticsViewModel.cs | 16 +++++
src/Wisp.App/LegacyMainWindow.xaml | 9 +++
src/Wisp.App/MainWindow.xaml | 12 ++++
.../NativeRendering/AnalogHudRenderWorker.cs | 24 ++++++++
.../DirectCompositionAnalogHost.cs | 30 +++++++++-
.../DirectCompositionDevice.cs | 18 +++++-
src/Wisp.App/NativeRendering/HudNativeHost.cs | 1 +
.../Supplementary/FeatureControlPolicy.cs | 2 +-
.../Supplementary/SupplementaryEnvironment.cs | 2 +
.../NativeContractTests.cpp | 40 +++++++++++++
.../Wisp.NativeRenderer.cpp | 33 ++++++++++
src/Wisp.NativeRenderer/Wisp.NativeRenderer.h | 10 +++-
.../CpuRenderingSettingsTests.cs | 21 ++++++-
.../FeatureControls/service-schema-1.json | 3 +-
.../HudGpuPrioritySettingsTests.cs | 60 +++++++++++++++++++
.../SupplementaryEnvironmentTests.cs | 2 +
20 files changed, 306 insertions(+), 9 deletions(-)
create mode 100644 tests/Wisp.App.Tests/HudGpuPrioritySettingsTests.cs
diff --git a/src/Wisp.App/AppController.FeatureControls.cs b/src/Wisp.App/AppController.FeatureControls.cs
index 912bf737..37feff85 100644
--- a/src/Wisp.App/AppController.FeatureControls.cs
+++ b/src/Wisp.App/AppController.FeatureControls.cs
@@ -72,6 +72,7 @@ internal Task ApplyFeatureControlsAsync(FeatureControlState state)
_ = ConfigureClipShortcut(false, settings.ToggleShortcutEnabled, settings.ToggleShortcut);
_ = ConfigureClipShortcut(true, settings.SaveShortcutEnabled, settings.SaveShortcut);
}
+ ApplyHudGpuPriority();
FeatureControlsChanged?.Invoke(this, EventArgs.Empty);
UpdateShiftCueObservation();
Overlay?.RefreshFeatureControls();
diff --git a/src/Wisp.App/AppController.cs b/src/Wisp.App/AppController.cs
index 515ad1e7..3adba74c 100644
--- a/src/Wisp.App/AppController.cs
+++ b/src/Wisp.App/AppController.cs
@@ -280,6 +280,28 @@ public void SetCpuRenderingEnabled(bool enabled)
ViewModel.UpdateCpuRendering(Settings.CpuRenderingEnabled, saved);
}
+ public void SetHudGpuPriorityEnabled(bool enabled)
+ {
+ if (_disposed) return;
+ var previous = Settings.HudGpuPriority;
+ Settings.HudGpuPriority = enabled;
+ var saved = SaveSettings();
+ if (!saved) Settings.HudGpuPriority = previous;
+ PublishSupplementarySettings();
+ ApplyHudGpuPriority(saved);
+ }
+
+ // Effective only when the user turned it on and Tools has not paused it for this version.
+ internal void ApplyHudGpuPriority(bool saved = true)
+ {
+ var active = Settings.HudGpuPriority && FeatureAllowed("gpu-priority");
+ ViewModel.UpdateHudGpuPriority(Settings.HudGpuPriority, active,
+ !saved ? "Could not save the renderer option. Try again."
+ : Settings.HudGpuPriority && !active
+ ? FeatureControls.Reason("gpu-priority") ?? "GPU priority is temporarily paused."
+ : "");
+ }
+
public async Task SetDebugLoggingEnabledAsync(bool enabled)
{
if (_disposed)
diff --git a/src/Wisp.App/AppSettings.cs b/src/Wisp.App/AppSettings.cs
index e8122e8a..5f23600b 100644
--- a/src/Wisp.App/AppSettings.cs
+++ b/src/Wisp.App/AppSettings.cs
@@ -127,6 +127,8 @@ public double TorqueGaugeScale
public bool AutomaticApplicationUpdateChecks { get; set; } = true;
public DateTimeOffset? LastApplicationUpdateCheckUtc { get; set; }
public bool CpuRenderingEnabled { get; set; }
+ // Off by default: the game keeps the GPU unless the user chooses otherwise.
+ public bool HudGpuPriority { get; set; }
public bool DebugLoggingEnabled { get; set; }
public DateTimeOffset? DebugLoggingExpiresAtUtc { get; set; }
public string ColorTheme { get; set; } = AppColorThemes.DefaultName;
diff --git a/src/Wisp.App/ControlPanelWindow.cs b/src/Wisp.App/ControlPanelWindow.cs
index eeec37d9..7597f3cf 100644
--- a/src/Wisp.App/ControlPanelWindow.cs
+++ b/src/Wisp.App/ControlPanelWindow.cs
@@ -73,6 +73,7 @@ public abstract partial class ControlPanelWindow : Window
private Button OverlayHotkeyCaptureButton => FindControl
-## Wisp 2.6.11
+## Wisp 2.6.12
-Wisp 2.6.11 brings conversations with Views into a compact chat popup, with message history, replies and optional diagnostic attachments. Available updates stay visible on the dashboard with a working Update action.
+Wisp 2.6.12 adds a HUD setting that decides whether HUD updates take priority over the game on the GPU, off by default, and keeps tune performance results with your current tune after you leave Forza's Tune screen. Closing Wisp completes promptly and clip actions report problems on the Clips page.
-[Clips](#clips) · [Tune](#tune) · [Lap review](#lap-review) · [Live map and lap delta](#live-map-and-lap-delta) · [Install](#install) · [2.6.11 release notes](docs/releases/Wisp-2.6.11-release-notes.md)
+[Clips](#clips) · [Tune](#tune) · [Lap review](#lap-review) · [Live map and lap delta](#live-map-and-lap-delta) · [Install](#install) · [2.6.12 release notes](docs/releases/Wisp-2.6.12-release-notes.md)
### Clips
diff --git a/src/Wisp.App/app.manifest b/src/Wisp.App/app.manifest
index 13231211..f7794721 100644
--- a/src/Wisp.App/app.manifest
+++ b/src/Wisp.App/app.manifest
@@ -1,6 +1,6 @@
-
+
From 65c822de7fc6d3aa0833837a07b0db85c4b567d7 Mon Sep 17 00:00:00 2001
From: Views2k <292889387+Views2k@users.noreply.github.com>
Date: Fri, 16 Oct 2026 03:24:39 +0000
Subject: [PATCH 6/8] Describe the 2.6.12 GPU setting and feature-pause change
accurately
The GPU setting gives HUD updates priority under heavy GPU load; the needle-speed claim was never measured. Feature pauses gained reporting to Tools only, which is not a user-visible change, so the release notes no longer list it.
---
CHANGELOG.md | 2 +-
docs/releases/Wisp-2.6.12-release-notes.md | 3 +--
src/Wisp.App/ReleaseNotesCatalog.cs | 5 ++---
3 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 87493aba..a8bc2ed0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@ Notable changes to Wisp are recorded here.
## 2.6.12 - 2026-10-16
-- Add Prioritize HUD updates on the GPU, a HUD setting that is off by default so the game keeps the GPU; turn it on for the fastest needle under heavy GPU load.
+- Add Prioritize HUD updates on the GPU, a HUD setting that is off by default so the game keeps the GPU; turn it on to give HUD updates priority when the GPU is under heavy load.
- Keep tune performance results with the current tune after leaving Forza's Tune screen, as long as the car, parts and settings are unchanged.
- Report feature pauses from Wisp Tools when they apply and when a policy is rejected or cannot be cached.
- Complete application exit promptly even when a renderer thread is busy.
diff --git a/docs/releases/Wisp-2.6.12-release-notes.md b/docs/releases/Wisp-2.6.12-release-notes.md
index bb1c778d..b419bc7e 100644
--- a/docs/releases/Wisp-2.6.12-release-notes.md
+++ b/docs/releases/Wisp-2.6.12-release-notes.md
@@ -5,7 +5,7 @@
## HUD GPU priority
- Prioritize HUD updates on the GPU is a new HUD setting, off by default, so the game keeps the GPU to itself.
-- Turn it on next to CPU rendering when you want the fastest needle under heavy GPU load. It applies immediately.
+- Turn it on next to CPU rendering to give HUD updates priority when the GPU is under heavy load. It applies immediately.
## Tune performance results
@@ -16,4 +16,3 @@
- Closing Wisp completes promptly even when a renderer thread is busy.
- Clip actions report problems on the Clips page and can be retried.
-- Feature pauses from Wisp Tools apply as soon as they are received.
diff --git a/src/Wisp.App/ReleaseNotesCatalog.cs b/src/Wisp.App/ReleaseNotesCatalog.cs
index 5c8eaafc..7beb40f2 100644
--- a/src/Wisp.App/ReleaseNotesCatalog.cs
+++ b/src/Wisp.App/ReleaseNotesCatalog.cs
@@ -23,14 +23,13 @@ public static class ReleaseNotesCatalog
[
Group("HUD GPU priority",
"Prioritize HUD updates on the GPU is a new HUD setting, off by default, so the game keeps the GPU to itself.",
- "Turn it on next to CPU rendering when you want the fastest needle under heavy GPU load; it applies immediately."),
+ "Turn it on next to CPU rendering to give HUD updates priority when the GPU is under heavy load; it applies immediately."),
Group("Tune performance results",
"Performance results read from Forza's Tune screen stay with your current tune after you leave the screen.",
"Refresh the current tune at any time and keep comparing braking, grip, acceleration and top speed."),
Group("Reliability",
"Closing Wisp completes promptly even when a renderer thread is busy.",
- "Clip actions report problems on the Clips page and can be retried.",
- "Feature pauses from Wisp Tools apply as soon as they are received.")
+ "Clip actions report problems on the Clips page and can be retried.")
]),
new(
"2.6.11",
From 97954c92ecd4ca8a8b85d00abef1c15b8919b28b Mon Sep 17 00:00:00 2001
From: Views2k <292889387+Views2k@users.noreply.github.com>
Date: Fri, 16 Oct 2026 06:58:55 +0000
Subject: [PATCH 7/8] Let players give a Chat with Views message a subject
Every message arrived in the inbox titled "Chat with Views", so conversations could not be told apart. The chat window gains an optional Subject field (up to 100 characters) above the composer; it is sent as the report title and stays filled so follow-ups share it. Blank keeps the previous title.
---
CHANGELOG.md | 1 +
docs/releases/Wisp-2.6.12-release-notes.md | 4 ++++
.../ControlPanelWindow.SupportInbox.cs | 2 +-
src/Wisp.App/ReleaseNotesCatalog.cs | 2 ++
src/Wisp.App/SupportChatView.cs | 18 +++++++++++++-----
5 files changed, 21 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a8bc2ed0..a3043aa0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ Notable changes to Wisp are recorded here.
- Add Prioritize HUD updates on the GPU, a HUD setting that is off by default so the game keeps the GPU; turn it on to give HUD updates priority when the GPU is under heavy load.
- Keep tune performance results with the current tune after leaving Forza's Tune screen, as long as the car, parts and settings are unchanged.
- Report feature pauses from Wisp Tools when they apply and when a policy is rejected or cannot be cached.
+- Let Chat with Views messages carry a subject.
- Complete application exit promptly even when a renderer thread is busy.
- Report clip action problems on the Clips page and allow a retry.
diff --git a/docs/releases/Wisp-2.6.12-release-notes.md b/docs/releases/Wisp-2.6.12-release-notes.md
index b419bc7e..b3429210 100644
--- a/docs/releases/Wisp-2.6.12-release-notes.md
+++ b/docs/releases/Wisp-2.6.12-release-notes.md
@@ -12,6 +12,10 @@
- Performance results read from Forza's Tune screen stay with your current tune after you leave the screen.
- Refresh the current tune at any time and keep comparing braking, grip, acceleration and top speed.
+## Chat with Views
+
+- Add a subject to your messages so they are easy to find later.
+
## Reliability
- Closing Wisp completes promptly even when a renderer thread is busy.
diff --git a/src/Wisp.App/ControlPanelWindow.SupportInbox.cs b/src/Wisp.App/ControlPanelWindow.SupportInbox.cs
index 33ac576a..099b1c02 100644
--- a/src/Wisp.App/ControlPanelWindow.SupportInbox.cs
+++ b/src/Wisp.App/ControlPanelWindow.SupportInbox.cs
@@ -60,7 +60,7 @@ private void OpenSupportChat()
if (_chatWindow is null)
{
_chatView = new SupportChatView(
- (message, diagnostics) => Task.Run(() => app.CreateSupplementarySupportPreview("feedback", "Chat with Views", message, diagnostics)),
+ (title, message, diagnostics) => Task.Run(() => app.CreateSupplementarySupportPreview("feedback", title, message, diagnostics)),
(report, cancellation) => app.SubmitSupplementarySupportAsync(report, cancellation, chat: true),
(older, ids) => app.RefreshSupportChatAsync(older, ids));
var frame = _chatView.CreatePopupSurface(() => _chatWindow?.Hide(), () => _chatWindow?.DragMove());
diff --git a/src/Wisp.App/ReleaseNotesCatalog.cs b/src/Wisp.App/ReleaseNotesCatalog.cs
index 7beb40f2..f49883b3 100644
--- a/src/Wisp.App/ReleaseNotesCatalog.cs
+++ b/src/Wisp.App/ReleaseNotesCatalog.cs
@@ -27,6 +27,8 @@ public static class ReleaseNotesCatalog
Group("Tune performance results",
"Performance results read from Forza's Tune screen stay with your current tune after you leave the screen.",
"Refresh the current tune at any time and keep comparing braking, grip, acceleration and top speed."),
+ Group("Chat with Views",
+ "Add a subject to your messages so they are easy to find later."),
Group("Reliability",
"Closing Wisp completes promptly even when a renderer thread is busy.",
"Clip actions report problems on the Clips page and can be retried.")
diff --git a/src/Wisp.App/SupportChatView.cs b/src/Wisp.App/SupportChatView.cs
index ff16050c..e14a5d32 100644
--- a/src/Wisp.App/SupportChatView.cs
+++ b/src/Wisp.App/SupportChatView.cs
@@ -11,12 +11,17 @@ namespace Wisp.App;
internal sealed class SupportChatView : Grid
{
- private readonly Func> _prepare;
+ private readonly Func> _prepare;
private readonly Func> _submit;
private readonly Func _refresh;
private readonly StackPanel _messages = new() { Margin = new Thickness(18, 8, 18, 8) };
private readonly ScrollViewer _scroll = new() { VerticalScrollBarVisibility = ScrollBarVisibility.Auto, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled };
internal TextBox Composer { get; } = new() { AcceptsReturn = true, TextWrapping = TextWrapping.Wrap, MaxLength = 4000, MinHeight = 64, MaxHeight = 120, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, Padding = new Thickness(10) };
+ // An optional subject that Views sees as the title in the inbox; it stays put so follow-ups share it.
+ internal TextBox Subject { get; } = new() { MaxLength = 100, Padding = new Thickness(10, 6, 10, 6), Margin = new Thickness(0, 0, 0, 6) };
+ private readonly TextBlock _subjectLabel = Text("Subject (optional)", true);
+ internal const string DefaultTitle = "Chat with Views";
+ private string SubjectOrDefault => string.IsNullOrWhiteSpace(Subject.Text) ? DefaultTitle : Subject.Text.Trim();
internal CheckBox Diagnostics { get; } = new() { Content = "Attach diagnostics", Margin = new Thickness(0), VerticalAlignment = VerticalAlignment.Center };
internal Button Send { get; } = Button("Send", true);
private readonly Button _edit = Button("Back"), _older = Button("Earlier messages");
@@ -44,7 +49,7 @@ internal Border CreatePopupSurface(Action close, Action drag)
return frame;
}
- internal SupportChatView(Func> prepare,
+ internal SupportChatView(Func> prepare,
Func> submit,
Func refresh)
{
@@ -58,8 +63,11 @@ internal SupportChatView(Func> p
SetRow(bottom, 1); Children.Add(bottom);
_status.Margin = new Thickness(0, 0, 0, 8); _status.Visibility = Visibility.Collapsed;
AutomationProperties.SetLiveSetting(_status, AutomationLiveSetting.Polite);
- bottom.Children.Add(_status); bottom.Children.Add(Composer);
+ _subjectLabel.Margin = new Thickness(0, 0, 0, 4);
+ bottom.Children.Add(_status); bottom.Children.Add(_subjectLabel); bottom.Children.Add(Subject); bottom.Children.Add(Composer);
+ AutomationProperties.SetName(Subject, "Subject for Views");
AutomationProperties.SetName(Composer, "Message to Views");
+ Subject.PreviewKeyDown += (_, e) => { if (e.Key == Key.Enter) { e.Handled = true; Composer.Focus(); } };
Diagnostics.ToolTip = "Review and attach up to 20 minutes of diagnostics.";
Diagnostics.SetResourceReference(StyleProperty, "ToggleSwitchStyle");
Diagnostics.SetResourceReference(Control.ForegroundProperty, "TextBrush");
@@ -167,7 +175,7 @@ private async Task SendAsync()
{
if (_pending is null)
{
- _pending = await _prepare(Composer.Text.Trim(), Diagnostics.IsChecked == true);
+ _pending = await _prepare(SubjectOrDefault, Composer.Text.Trim(), Diagnostics.IsChecked == true);
if (_pending is null) { SetStatus("Message unavailable. Your draft is kept; try again."); return; }
if (Diagnostics.IsChecked == true || _pending.Message != Composer.Text.Trim())
{
@@ -208,7 +216,7 @@ private void Review(bool show)
}
private void UpdateActions()
{
- Composer.IsReadOnly = _busy || _pending is not null;
+ Composer.IsReadOnly = Subject.IsReadOnly = _busy || _pending is not null;
Diagnostics.IsEnabled = !_busy && _pending is null && _enabled;
Send.IsEnabled = !_busy && _enabled && !string.IsNullOrWhiteSpace(Composer.Text);
Send.Content = _busy ? "Sending…" : _receiptPending ? "Retry" : _pending is not null ? "Send" : Diagnostics.IsChecked == true ? "Review & send" : "Send";
From c29f584c26284c280da21dec7b2b16a8ae94f254 Mon Sep 17 00:00:00 2001
From: Views2k <292889387+Views2k@users.noreply.github.com>
Date: Fri, 16 Oct 2026 07:00:29 +0000
Subject: [PATCH 8/8] Pass the subject through the UI review chat fixture
---
tools/Wisp.UiReview/SupportComposerReview.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/Wisp.UiReview/SupportComposerReview.cs b/tools/Wisp.UiReview/SupportComposerReview.cs
index ea6c0647..00b6b951 100644
--- a/tools/Wisp.UiReview/SupportComposerReview.cs
+++ b/tools/Wisp.UiReview/SupportComposerReview.cs
@@ -34,7 +34,7 @@ internal static int RunChat(string output, Func resources)
foreach (var width in new[] { 360, 480, 640 })
{
var submitted = new List(); var fail = true;
- var view = new SupportChatView((text, diagnostics) => Task.FromResult(new(1, Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), now, "2.6.11", "fixture", "stable", "feedback", "Chat", text)),
+ var view = new SupportChatView((title, text, diagnostics) => Task.FromResult(new(1, Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), now, "2.6.11", "fixture", "stable", "feedback", title, text)),
(report, _) => { submitted.Add(report.EventId); return Task.FromResult(new SupplementarySupportResult(fail ? SupplementaryRequestStatus.Unavailable : SupplementaryRequestStatus.Success)); },
(_, _) => Task.CompletedTask);
var surface = view.CreatePopupSurface(() => { }, () => { });