Skip to content

Commit 2a0a612

Browse files
shanselmanclaude
andcommitted
Code simplification and fix Gemini hook format
Simplifications: - Add to_lower() utility function (eliminates 4 duplicate patterns) - Add HandleGuard RAII wrapper for safe handle cleanup - Refactor detect_preset_from_ancestors() to use HandleGuard - Refactor find_ancestor_window() to use HandleGuard - Simplify find_preset() using to_lower() Bug fix: - Gemini CLI requires nested hooks array (same as Claude) - Update README with correct Gemini hook format Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent b67ed51 commit 2a0a612

File tree

2 files changed

+43
-27
lines changed

2 files changed

+43
-27
lines changed

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,13 @@ Add to `~/.gemini/settings.json`:
136136
"hooks": {
137137
"AfterAgent": [
138138
{
139-
"type": "command",
140-
"command": "C:\\path\\to\\toasty.exe \"Gemini finished\"",
141-
"timeout": 5000
139+
"hooks": [
140+
{
141+
"type": "command",
142+
"command": "C:\\path\\to\\toasty.exe \"Gemini finished\"",
143+
"timeout": 5000
144+
}
145+
]
142146
}
143147
]
144148
}

main.cpp

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ const wchar_t* APP_ID = L"Toasty.CLI.Notification";
3131
const wchar_t* APP_NAME = L"Toasty";
3232
const 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+
3443
struct 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)
88103
const 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
162173
const 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
190200
const 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
429435
HWND 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

Comments
 (0)