@@ -31,6 +31,15 @@ const wchar_t* APP_ID = L"Toasty.CLI.Notification";
3131const wchar_t * APP_NAME = L" Toasty" ;
3232const wchar_t * PROTOCOL_NAME = L" toasty" ;
3333
34+ // RAII wrapper for Windows handles
35+ struct HandleGuard {
36+ HANDLE h;
37+ HandleGuard (HANDLE handle) : h(handle) {}
38+ ~HandleGuard () { if (h && h != INVALID_HANDLE_VALUE) CloseHandle (h); }
39+ operator HANDLE () const { return h; }
40+ bool valid () const { return h && h != INVALID_HANDLE_VALUE; }
41+ };
42+
3443struct AppPreset {
3544 std::wstring name;
3645 std::wstring title;
@@ -84,15 +93,17 @@ std::wstring extract_icon_to_temp(int resourceId) {
8493 }
8594}
8695
96+ // Utility: Convert string to lowercase
97+ std::wstring to_lower (std::wstring str) {
98+ for (auto & c : str) c = towlower (c);
99+ return str;
100+ }
101+
87102// Find preset by name (case-insensitive)
88103const AppPreset* find_preset (const std::wstring& name) {
89- std::wstring lowerName = name;
90- for (auto & c : lowerName) c = towlower (c);
91-
104+ auto lowerName = to_lower (name);
92105 for (const auto & preset : APP_PRESETS) {
93- std::wstring presetName = preset.name ;
94- for (auto & c : presetName) c = towlower (c);
95- if (presetName == lowerName) {
106+ if (to_lower (preset.name ) == lowerName) {
96107 return &preset;
97108 }
98109 }
@@ -160,8 +171,7 @@ std::wstring get_process_command_line(DWORD pid) {
160171
161172// Check if command line contains a known CLI pattern
162173const AppPreset* check_command_line_for_preset (const std::wstring& cmdLine) {
163- std::wstring lowerCmd = cmdLine;
164- for (auto & c : lowerCmd) c = towlower (c);
174+ auto lowerCmd = to_lower (cmdLine);
165175
166176 // Check for Gemini CLI (multiple patterns)
167177 if (lowerCmd.find (L" gemini-cli" ) != std::wstring::npos ||
@@ -188,8 +198,8 @@ const AppPreset* check_command_line_for_preset(const std::wstring& cmdLine) {
188198
189199// Walk up process tree to find a matching AI CLI preset
190200const AppPreset* detect_preset_from_ancestors (bool debug = false ) {
191- HANDLE snapshot = CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0 );
192- if (snapshot == INVALID_HANDLE_VALUE ) {
201+ HandleGuard snapshot ( CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0 ) );
202+ if (! snapshot. valid () ) {
193203 return nullptr ;
194204 }
195205
@@ -232,8 +242,7 @@ const AppPreset* detect_preset_from_ancestors(bool debug = false) {
232242 }
233243
234244 // Convert to lowercase for matching
235- std::wstring lowerExeName = exeName;
236- for (auto & c : lowerExeName) c = towlower (c);
245+ auto lowerExeName = to_lower (exeName);
237246
238247 // Get command line for this process
239248 std::wstring cmdLine = get_process_command_line (parentPid);
@@ -248,15 +257,13 @@ const AppPreset* detect_preset_from_ancestors(bool debug = false) {
248257 const AppPreset* preset = find_preset (lowerExeName);
249258 if (preset) {
250259 if (debug) std::wcerr << L" [DEBUG] MATCH by name: " << lowerExeName << L" \n " ;
251- CloseHandle (snapshot);
252260 return preset;
253261 }
254262
255263 // Check command line for CLI patterns (handles node.exe, etc.)
256264 preset = check_command_line_for_preset (cmdLine);
257265 if (preset) {
258266 if (debug) std::wcerr << L" [DEBUG] MATCH by cmdline\n " ;
259- CloseHandle (snapshot);
260267 return preset;
261268 }
262269
@@ -269,7 +276,6 @@ const AppPreset* detect_preset_from_ancestors(bool debug = false) {
269276 currentPid = parentPid;
270277 }
271278
272- CloseHandle (snapshot);
273279 return nullptr ;
274280}
275281
@@ -427,8 +433,8 @@ HWND find_window_for_process(DWORD pid) {
427433
428434// Walk process tree to find the terminal/IDE window that launched us
429435HWND find_ancestor_window () {
430- HANDLE snapshot = CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0 );
431- if (snapshot == INVALID_HANDLE_VALUE ) {
436+ HandleGuard snapshot ( CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0 ) );
437+ if (! snapshot. valid () ) {
432438 return nullptr ;
433439 }
434440
@@ -457,14 +463,12 @@ HWND find_ancestor_window() {
457463 // Check if this parent has a visible window
458464 HWND hwnd = find_window_for_process (parentPid);
459465 if (hwnd) {
460- CloseHandle (snapshot);
461466 return hwnd;
462467 }
463468
464469 currentPid = parentPid;
465470 }
466471
467- CloseHandle (snapshot);
468472 return nullptr ;
469473}
470474
@@ -766,14 +770,21 @@ bool install_gemini(const std::wstring& exePath) {
766770 }
767771 }
768772
769- // Build hook structure (Gemini uses flat structure, no nested "hooks" array)
770- JsonObject hookItem;
771- hookItem.SetNamedValue (L" type" , JsonValue::CreateStringValue (L" command" ));
773+ // Build hook structure with nested "hooks" array (required by Gemini CLI)
774+ // Structure: hooks -> AfterAgent -> [ { hooks: [ { command: ... } ] } ]
775+ JsonObject innerHook;
776+ innerHook.SetNamedValue (L" type" , JsonValue::CreateStringValue (L" command" ));
772777
773778 std::wstring escapedPath = escape_json_string (exePath);
774779 std::wstring command = escapedPath + L" \" Gemini finished\" -t \" Gemini\" " ;
775- hookItem.SetNamedValue (L" command" , JsonValue::CreateStringValue (command));
776- hookItem.SetNamedValue (L" timeout" , JsonValue::CreateNumberValue (5000 ));
780+ innerHook.SetNamedValue (L" command" , JsonValue::CreateStringValue (command));
781+ innerHook.SetNamedValue (L" timeout" , JsonValue::CreateNumberValue (5000 ));
782+
783+ JsonArray innerHooks;
784+ innerHooks.Append (innerHook);
785+
786+ JsonObject hookItem;
787+ hookItem.SetNamedValue (L" hooks" , innerHooks);
777788
778789 // Get or create hooks object
779790 JsonObject hooksObj;
@@ -785,6 +796,7 @@ bool install_gemini(const std::wstring& exePath) {
785796 JsonArray afterAgentArray;
786797 if (hooksObj.HasKey (L" AfterAgent" )) {
787798 afterAgentArray = hooksObj.GetNamedArray (L" AfterAgent" );
799+ // Check if our hook is already installed (check nested structure)
788800 if (has_toasty_hook (afterAgentArray)) {
789801 return true ; // Already installed
790802 }
0 commit comments