Skip to content

Commit 480b362

Browse files
fix(embed): put back embeds that move themselves
Apps can move, resize, or minimize the embedded window on their own (Blender .blend size, Win+arrow). Comparing only to our last request hid those drifts, and SetWindowPos left iconic/zoomed state untouched. Match the live rect and clear min/max first so the next tick re-covers the host. - grill_me: final plan summary is plain chat, not an ask round
1 parent f03b588 commit 480b362

3 files changed

Lines changed: 54 additions & 7 deletions

File tree

‎src/EmbeddedWindowWorker.cpp‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ void EmbeddedWindowWorker::State::run(const std::shared_ptr<State> &self)
7979
if (!s.attached)
8080
return;
8181
quint64 appliedStamp = 0;
82-
quint64 appliedRect = ~quint64(0);
8382
quint64 appliedActivate = 0;
8483
quint64 appliedForeground = 0;
8584
int appliedEnabled = -1;
@@ -103,10 +102,20 @@ void EmbeddedWindowWorker::State::run(const std::shared_ptr<State> &self)
103102
const quint64 g = s.geometry.load(std::memory_order_relaxed);
104103
const auto field = [g](int shift) { return static_cast<int>(static_cast<qint16>((g >> shift) & 0xffff)); };
105104
const bool visible = stamp & 1;
106-
// Same rect (a tab switch): only show/hide, so the app gets no
107-
// move/size messages at all.
108-
const UINT keep = g == appliedRect ? SWP_NOMOVE | SWP_NOSIZE : 0;
109-
appliedRect = g;
105+
// The app minimized or maximized itself (Win+arrow on the
106+
// foreground embed): leave that state through its own window
107+
// procedure first, or SetWindowPos only moves the restore rect.
108+
if (visible && (IsIconic(s.target) || IsZoomed(s.target)))
109+
ShowWindow(s.target, SW_SHOWNOACTIVATE);
110+
// Already where it should be (a tab switch): only show/hide, so
111+
// the app gets no move/size messages at all. Compare with where
112+
// the window is, never with what we last asked for: an app that
113+
// moved itself must be put back by the next request.
114+
RECT current{};
115+
const bool sameRect = GetWindowRect(s.target, &current)
116+
&& current.left == field(0) && current.top == field(16)
117+
&& current.right - current.left == field(32) && current.bottom - current.top == field(48);
118+
const UINT keep = sameRect ? SWP_NOMOVE | SWP_NOSIZE : 0;
110119
// NOZORDER: an owned window always stays above its owner; keep its
111120
// place among our other owned windows (dialogs, menus, docks).
112121
const UINT flags = SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER | keep;

‎src/GrillMeForm.cpp‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,12 @@ QJsonObject toolDefinition()
639639
"`[grill_me answers]`; `no answer` = skipped, re-ask later; `needs clarification` = explain and "
640640
"re-ask only those questions (same ids), the other answers are final. A chat reply replaces the "
641641
"form. Once the plan is settled or the user said to implement, stop asking: never ask how or "
642-
"whether to proceed or offer invented next-step menus.")},
642+
"whether to proceed or offer invented next-step menus. The closing check (\"is this the shared "
643+
"understanding?\", the final plan summary to accept) is never a form question: write the settled "
644+
"plan as plain text in your reply and let the user confirm or correct it in chat. Once you have "
645+
"written that summary, the rounds are over: do not call this tool again for the rest of that "
646+
"work (implementation, follow-up questions, next steps) unless the user starts a new question "
647+
"round.")},
643648
{QStringLiteral("inputSchema"), schema},
644649
};
645650
}
@@ -661,7 +666,9 @@ constexpr QLatin1String kQuestionRoundCommands[] = {
661666

662667
constexpr QLatin1String kQuestionRoundHint(
663668
"[NotepadAI note, not part of the request] Deliver each question round of this command through the "
664-
"`ask` tool of the `grill_me` MCP server (one call per round, then end your turn), never as plain text.");
669+
"`ask` tool of the `grill_me` MCP server (one call per round, then end your turn), never as plain text. "
670+
"The final plan summary and its confirmation are not a round: write them as plain text, not through `ask`, "
671+
"and never call `ask` again after that summary.");
665672

666673
} // namespace
667674

‎tests/test_embedded_window_win32.cpp‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ private slots:
255255
void embedIsOwnedPopupOutOfTaskbar();
256256
void followsWindowMoves();
257257
void hidesWithItsTab();
258+
void putsBackAnEmbedThatMovedItself();
259+
void restoresAnEmbedThatMinimizedItself();
258260
void modalDialogDisablesEmbed();
259261
void blenderGetsModifierKeys();
260262
void benchmarkNativeSiblingCost();
@@ -975,6 +977,35 @@ void TestEmbeddedWindowWin32::hidesWithItsTab()
975977
QTRY_VERIFY_WITH_TIMEOUT(hostFilledByChild(), 1500);
976978
}
977979

980+
// The app moves or resizes its own window (Blender applies the window size a
981+
// .blend stores, Win+arrow snaps the foreground embed): the watchdog tick
982+
// (EmbeddedWindowManager::platTick -> sync) must put it back over the host,
983+
// even though the host itself did not move.
984+
void TestEmbeddedWindowWin32::putsBackAnEmbedThatMovedItself()
985+
{
986+
QVERIFY(embed());
987+
QTRY_VERIFY_WITH_TIMEOUT(hostFilledByChild(), 1500);
988+
HWND t = h->target.hwnd();
989+
RECT r{};
990+
QVERIFY(GetWindowRect(t, &r));
991+
QVERIFY(SetWindowPos(t, nullptr, r.left + 400, r.top + 300, 200, 150,
992+
SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER));
993+
QVERIFY(!hostFilledByChild(false));
994+
QTRY_VERIFY_WITH_TIMEOUT((h->host->sync(), hostFilledByChild()), 1500);
995+
}
996+
997+
// The app minimizes itself (Win+Down on the foreground embed): the host stays
998+
// empty unless the tick restores it.
999+
void TestEmbeddedWindowWin32::restoresAnEmbedThatMinimizedItself()
1000+
{
1001+
QVERIFY(embed());
1002+
QTRY_VERIFY_WITH_TIMEOUT(hostFilledByChild(), 1500);
1003+
HWND t = h->target.hwnd();
1004+
ShowWindow(t, SW_SHOWMINNOACTIVE);
1005+
QTRY_VERIFY_WITH_TIMEOUT(IsIconic(t), 1000);
1006+
QTRY_VERIFY_WITH_TIMEOUT((h->host->sync(), hostFilledByChild()), 1500);
1007+
}
1008+
9781009
void TestEmbeddedWindowWin32::modalDialogDisablesEmbed()
9791010
{
9801011
QVERIFY(embed());

0 commit comments

Comments
 (0)