@hayasaka7/haya-pet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +34 -0
- package/.github/workflows/release.yml +61 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/apps/cli/src/haya-pet.js +395 -0
- package/apps/cli/test/haya-pet.test.mjs +339 -0
- package/apps/companion/README.md +83 -0
- package/apps/companion/package.json +17 -0
- package/apps/companion/src/main/display-manager.js +71 -0
- package/apps/companion/src/main/index.js +349 -0
- package/apps/companion/src/main/lock-file.js +52 -0
- package/apps/companion/src/main/panel-placement.js +45 -0
- package/apps/companion/src/main/pet-loader.js +2 -0
- package/apps/companion/src/main/position-store.js +3 -0
- package/apps/companion/src/main/preload.cjs +13 -0
- package/apps/companion/src/main/state-file.js +2 -0
- package/apps/companion/src/main/terminal-helper-client.js +79 -0
- package/apps/companion/src/main/terminal-locator.js +44 -0
- package/apps/companion/src/main/tray-menu.js +79 -0
- package/apps/companion/src/main/window-options.js +66 -0
- package/apps/companion/src/renderer/index.html +18 -0
- package/apps/companion/src/renderer/interaction-controller.js +114 -0
- package/apps/companion/src/renderer/pet-window.js +275 -0
- package/apps/companion/src/renderer/session-bubbles.js +138 -0
- package/apps/companion/src/renderer/styles.css +225 -0
- package/apps/companion/src/renderer/task-talk-window.js +141 -0
- package/apps/companion/test/display-manager.test.mjs +48 -0
- package/apps/companion/test/interaction-controller.test.mjs +107 -0
- package/apps/companion/test/panel-placement.test.mjs +60 -0
- package/apps/companion/test/position-store.test.mjs +54 -0
- package/apps/companion/test/state-file.test.mjs +52 -0
- package/apps/companion/test/terminal-helper-client.test.mjs +68 -0
- package/apps/companion/test/terminal-locator.test.mjs +35 -0
- package/apps/companion/test/tray-menu.test.mjs +45 -0
- package/apps/companion/test/window-options.test.mjs +62 -0
- package/apps/pet-preview/index.html +42 -0
- package/apps/pet-preview/src/preview-app.js +123 -0
- package/apps/pet-preview/src/preview-state.js +70 -0
- package/apps/pet-preview/src/preview.css +125 -0
- package/apps/pet-preview/test/preview-state.test.mjs +62 -0
- package/assets/fallback-pet/README.md +16 -0
- package/assets/fallback-pet/pet.json +13 -0
- package/docs/architecture.md +144 -0
- package/docs/known-issues.md +49 -0
- package/docs/publishing.md +48 -0
- package/docs/screenshots/README.md +7 -0
- package/docs/screenshots/folder-collapsed.png +0 -0
- package/docs/screenshots/hero.png +0 -0
- package/docs/screenshots/pet-overlay.png +0 -0
- package/docs/screenshots/session-bubbles.png +0 -0
- package/docs/screenshots/tray-menu.png +0 -0
- package/docs/troubleshooting.md +36 -0
- package/native/README.md +80 -0
- package/native/linux-window-helper/README.md +29 -0
- package/native/mac-window-helper/README.md +30 -0
- package/native/win-window-helper/Program.cs +312 -0
- package/native/win-window-helper/README.md +53 -0
- package/native/win-window-helper/win-window-helper.csproj +12 -0
- package/package.json +35 -0
- package/packages/adapters/src/adapter-info.js +61 -0
- package/packages/adapters/src/capabilities.js +39 -0
- package/packages/adapters/src/heuristics.js +114 -0
- package/packages/adapters/src/output-observer.js +164 -0
- package/packages/adapters/src/routing.js +86 -0
- package/packages/adapters/test/adapter-info.test.mjs +35 -0
- package/packages/adapters/test/capabilities.test.mjs +44 -0
- package/packages/adapters/test/heuristics.test.mjs +42 -0
- package/packages/adapters/test/output-observer.test.mjs +142 -0
- package/packages/adapters/test/routing.test.mjs +93 -0
- package/packages/app-state/src/state-file.js +53 -0
- package/packages/app-state/src/state.js +80 -0
- package/packages/app-state/test/state.test.mjs +36 -0
- package/packages/cli-core/src/companion-launcher.js +69 -0
- package/packages/cli-core/src/pty-runner.js +96 -0
- package/packages/cli-core/src/run-command.js +353 -0
- package/packages/cli-core/src/strip-ansi.js +16 -0
- package/packages/cli-core/test/companion-launcher.test.mjs +98 -0
- package/packages/cli-core/test/run-command.test.mjs +177 -0
- package/packages/cli-core/test/strip-ansi.test.mjs +27 -0
- package/packages/daemon-core/src/daemon-runtime.js +49 -0
- package/packages/daemon-core/src/ipc-server.js +180 -0
- package/packages/daemon-core/src/ipc-transport.js +70 -0
- package/packages/daemon-core/src/singleton.js +46 -0
- package/packages/daemon-core/test/daemon-runtime.test.mjs +65 -0
- package/packages/daemon-core/test/ipc-server.test.mjs +70 -0
- package/packages/daemon-core/test/ipc-transport.test.mjs +72 -0
- package/packages/daemon-core/test/singleton.test.mjs +32 -0
- package/packages/pet-core/src/animation-state.js +84 -0
- package/packages/pet-core/src/animator.js +26 -0
- package/packages/pet-core/src/atlas.js +81 -0
- package/packages/pet-core/src/discovery.js +90 -0
- package/packages/pet-core/src/manifest.js +112 -0
- package/packages/pet-core/src/validation.js +43 -0
- package/packages/pet-core/test/animation-state.test.mjs +47 -0
- package/packages/pet-core/test/animator.test.mjs +31 -0
- package/packages/pet-core/test/atlas.test.mjs +81 -0
- package/packages/pet-core/test/discovery.test.mjs +93 -0
- package/packages/pet-core/test/manifest.test.mjs +93 -0
- package/packages/pet-core/test/validation.test.mjs +69 -0
- package/packages/platform-core/src/capabilities.js +49 -0
- package/packages/platform-core/src/paths.js +75 -0
- package/packages/platform-core/src/platform.js +15 -0
- package/packages/platform-core/test/platform.test.mjs +84 -0
- package/packages/protocol/src/messages.js +156 -0
- package/packages/protocol/test/messages.test.mjs +112 -0
- package/packages/session-core/src/bubble-linger.js +47 -0
- package/packages/session-core/src/bubble-view.js +79 -0
- package/packages/session-core/src/pet-state.js +56 -0
- package/packages/session-core/src/priority.js +56 -0
- package/packages/session-core/src/registry.js +144 -0
- package/packages/session-core/src/summaries.js +54 -0
- package/packages/session-core/test/bubble-linger.test.mjs +96 -0
- package/packages/session-core/test/bubble-view.test.mjs +79 -0
- package/packages/session-core/test/pet-state.test.mjs +118 -0
- package/packages/session-core/test/priority.test.mjs +53 -0
- package/packages/session-core/test/registry.test.mjs +161 -0
- package/packages/session-core/test/summaries.test.mjs +38 -0
- package/packages/task-core/src/approvals.js +91 -0
- package/packages/task-core/src/controls.js +61 -0
- package/packages/task-core/src/replies.js +80 -0
- package/packages/task-core/src/task-events.js +101 -0
- package/packages/task-core/src/task-status.js +93 -0
- package/packages/task-core/src/task-store.js +74 -0
- package/packages/task-core/test/approvals.test.mjs +61 -0
- package/packages/task-core/test/controls.test.mjs +61 -0
- package/packages/task-core/test/replies.test.mjs +51 -0
- package/packages/task-core/test/task-events.test.mjs +67 -0
- package/packages/task-core/test/task-status.test.mjs +49 -0
- package/packages/task-core/test/task-store.test.mjs +65 -0
- package/test/harness.mjs +22 -0
- package/test/run-tests.mjs +47 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// Haya Pet — Windows window helper.
|
|
2
|
+
//
|
|
3
|
+
// Implements the line-delimited JSON helper protocol documented in
|
|
4
|
+
// ../README.md. Reads one JSON request per line on stdin and writes one JSON
|
|
5
|
+
// response per line on stdout. Diagnostics (if any) go to stderr only.
|
|
6
|
+
//
|
|
7
|
+
// Supported ops:
|
|
8
|
+
// { "id": "...", "op": "capabilities" }
|
|
9
|
+
// { "id": "...", "op": "locate", "pid": <int>, "terminalPid": <int?> }
|
|
10
|
+
|
|
11
|
+
using System.Runtime.InteropServices;
|
|
12
|
+
using System.Runtime.Versioning;
|
|
13
|
+
using System.Text;
|
|
14
|
+
using System.Text.Json;
|
|
15
|
+
|
|
16
|
+
[assembly: SupportedOSPlatform("windows")]
|
|
17
|
+
|
|
18
|
+
var jsonOptions = new JsonSerializerOptions
|
|
19
|
+
{
|
|
20
|
+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Per-monitor DPI awareness so GetWindowRect returns physical pixels the
|
|
24
|
+
// runtime can convert via its display manager. Best-effort; ignore failure.
|
|
25
|
+
try { Native.SetProcessDpiAwarenessContext(Native.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); }
|
|
26
|
+
catch { /* older Windows: ignore */ }
|
|
27
|
+
|
|
28
|
+
string? line;
|
|
29
|
+
while ((line = Console.ReadLine()) != null)
|
|
30
|
+
{
|
|
31
|
+
if (string.IsNullOrWhiteSpace(line))
|
|
32
|
+
{
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
object response;
|
|
37
|
+
try
|
|
38
|
+
{
|
|
39
|
+
response = Handle(line);
|
|
40
|
+
}
|
|
41
|
+
catch (Exception ex)
|
|
42
|
+
{
|
|
43
|
+
response = new { id = (string?)null, ok = false, error = "invalid_request", detail = ex.Message };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Console.WriteLine(JsonSerializer.Serialize(response, jsonOptions));
|
|
47
|
+
Console.Out.Flush();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
object Handle(string requestLine)
|
|
51
|
+
{
|
|
52
|
+
using var doc = JsonDocument.Parse(requestLine);
|
|
53
|
+
var root = doc.RootElement;
|
|
54
|
+
|
|
55
|
+
string? id = root.TryGetProperty("id", out var idEl) ? idEl.GetString() : null;
|
|
56
|
+
string op = root.TryGetProperty("op", out var opEl) ? opEl.GetString() ?? "" : "";
|
|
57
|
+
|
|
58
|
+
switch (op)
|
|
59
|
+
{
|
|
60
|
+
case "capabilities":
|
|
61
|
+
return new
|
|
62
|
+
{
|
|
63
|
+
id,
|
|
64
|
+
ok = true,
|
|
65
|
+
capabilities = new { locate = true, follow = false, permission = "granted" }
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
case "locate":
|
|
69
|
+
if (!root.TryGetProperty("pid", out var pidEl) || !pidEl.TryGetInt32(out int pid))
|
|
70
|
+
{
|
|
71
|
+
return new { id, ok = false, error = "invalid_request" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
int? terminalPid = root.TryGetProperty("terminalPid", out var tEl) && tEl.TryGetInt32(out int t) ? t : null;
|
|
75
|
+
var window = WindowLocator.Locate(pid, terminalPid);
|
|
76
|
+
return window is null
|
|
77
|
+
? new { id, ok = false, error = "not_found" }
|
|
78
|
+
: new { id, ok = true, window };
|
|
79
|
+
|
|
80
|
+
default:
|
|
81
|
+
return new { id, ok = false, error = "invalid_request" };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// <summary>Resolves the terminal window for an AI client process tree.</summary>
|
|
86
|
+
static class WindowLocator
|
|
87
|
+
{
|
|
88
|
+
// Process names that own a real terminal window, ranked first when matched.
|
|
89
|
+
private static readonly HashSet<string> KnownTerminals = new(StringComparer.OrdinalIgnoreCase)
|
|
90
|
+
{
|
|
91
|
+
"WindowsTerminal.exe", "powershell.exe", "pwsh.exe", "cmd.exe",
|
|
92
|
+
"Code.exe", "wezterm-gui.exe", "alacritty.exe", "conemu64.exe", "conemu.exe"
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
public static object? Locate(int pid, int? terminalPid)
|
|
96
|
+
{
|
|
97
|
+
var processNames = ProcessTree.SnapshotNames();
|
|
98
|
+
var parents = ProcessTree.SnapshotParents();
|
|
99
|
+
|
|
100
|
+
// Candidate pids: the client, an explicit terminal hint, and all ancestors.
|
|
101
|
+
var candidates = new HashSet<int>();
|
|
102
|
+
AddAncestors(pid, parents, candidates);
|
|
103
|
+
if (terminalPid is int hint)
|
|
104
|
+
{
|
|
105
|
+
AddAncestors(hint, parents, candidates);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
(IntPtr hWnd, int pidOwner, Native.RECT rect, string title)? best = null;
|
|
109
|
+
int bestScore = int.MinValue;
|
|
110
|
+
|
|
111
|
+
Native.EnumWindows((hWnd, _) =>
|
|
112
|
+
{
|
|
113
|
+
if (!Native.IsWindowVisible(hWnd))
|
|
114
|
+
{
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
Native.GetWindowThreadProcessId(hWnd, out uint wpid);
|
|
119
|
+
if (!candidates.Contains((int)wpid))
|
|
120
|
+
{
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!Native.GetWindowRect(hWnd, out var rect))
|
|
125
|
+
{
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
int width = rect.Right - rect.Left;
|
|
130
|
+
int height = rect.Bottom - rect.Top;
|
|
131
|
+
if (width <= 0 || height <= 0)
|
|
132
|
+
{
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
string title = GetTitle(hWnd);
|
|
137
|
+
if (title.Length == 0)
|
|
138
|
+
{
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
int score = Score((int)wpid, width, height, processNames);
|
|
143
|
+
if (score > bestScore)
|
|
144
|
+
{
|
|
145
|
+
bestScore = score;
|
|
146
|
+
best = (hWnd, (int)wpid, rect, title);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return true;
|
|
150
|
+
}, IntPtr.Zero);
|
|
151
|
+
|
|
152
|
+
if (best is null)
|
|
153
|
+
{
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
var b = best.Value;
|
|
158
|
+
bool knownTerminal = processNames.TryGetValue(b.pidOwner, out var name) && KnownTerminals.Contains(name);
|
|
159
|
+
|
|
160
|
+
return new
|
|
161
|
+
{
|
|
162
|
+
x = b.rect.Left,
|
|
163
|
+
y = b.rect.Top,
|
|
164
|
+
width = b.rect.Right - b.rect.Left,
|
|
165
|
+
height = b.rect.Bottom - b.rect.Top,
|
|
166
|
+
displayId = "primary",
|
|
167
|
+
title = b.title,
|
|
168
|
+
confidence = knownTerminal ? 0.8 : 0.5
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private static int Score(int pid, int width, int height, IReadOnlyDictionary<int, string> names)
|
|
173
|
+
{
|
|
174
|
+
// Prefer known terminal processes, then larger windows.
|
|
175
|
+
int terminalBonus = names.TryGetValue(pid, out var name) && KnownTerminals.Contains(name) ? 1_000_000_000 : 0;
|
|
176
|
+
return terminalBonus + Math.Min(width * height, 900_000_000);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private static void AddAncestors(int pid, IReadOnlyDictionary<int, int> parents, HashSet<int> into)
|
|
180
|
+
{
|
|
181
|
+
int current = pid;
|
|
182
|
+
int guard = 0;
|
|
183
|
+
while (current > 0 && into.Add(current) && guard++ < 64)
|
|
184
|
+
{
|
|
185
|
+
if (!parents.TryGetValue(current, out int parent) || parent == current)
|
|
186
|
+
{
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
current = parent;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private static string GetTitle(IntPtr hWnd)
|
|
194
|
+
{
|
|
195
|
+
int length = Native.GetWindowTextLengthW(hWnd);
|
|
196
|
+
if (length <= 0)
|
|
197
|
+
{
|
|
198
|
+
return "";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var sb = new StringBuilder(length + 1);
|
|
202
|
+
Native.GetWindowTextW(hWnd, sb, sb.Capacity);
|
|
203
|
+
return sb.ToString();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// <summary>Reads the live process table once per request via Toolhelp.</summary>
|
|
208
|
+
static class ProcessTree
|
|
209
|
+
{
|
|
210
|
+
public static Dictionary<int, int> SnapshotParents() => Snapshot().parents;
|
|
211
|
+
|
|
212
|
+
public static Dictionary<int, string> SnapshotNames() => Snapshot().names;
|
|
213
|
+
|
|
214
|
+
private static (Dictionary<int, int> parents, Dictionary<int, string> names) Snapshot()
|
|
215
|
+
{
|
|
216
|
+
var parents = new Dictionary<int, int>();
|
|
217
|
+
var names = new Dictionary<int, string>();
|
|
218
|
+
|
|
219
|
+
IntPtr snapshot = Native.CreateToolhelp32Snapshot(Native.TH32CS_SNAPPROCESS, 0);
|
|
220
|
+
if (snapshot == Native.INVALID_HANDLE_VALUE)
|
|
221
|
+
{
|
|
222
|
+
return (parents, names);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try
|
|
226
|
+
{
|
|
227
|
+
var entry = new Native.PROCESSENTRY32W { dwSize = (uint)Marshal.SizeOf<Native.PROCESSENTRY32W>() };
|
|
228
|
+
if (Native.Process32FirstW(snapshot, ref entry))
|
|
229
|
+
{
|
|
230
|
+
do
|
|
231
|
+
{
|
|
232
|
+
parents[(int)entry.th32ProcessID] = (int)entry.th32ParentProcessID;
|
|
233
|
+
names[(int)entry.th32ProcessID] = entry.szExeFile ?? "";
|
|
234
|
+
}
|
|
235
|
+
while (Native.Process32NextW(snapshot, ref entry));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
finally
|
|
239
|
+
{
|
|
240
|
+
Native.CloseHandle(snapshot);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return (parents, names);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
static class Native
|
|
248
|
+
{
|
|
249
|
+
public const uint TH32CS_SNAPPROCESS = 0x00000002;
|
|
250
|
+
public static readonly IntPtr INVALID_HANDLE_VALUE = new(-1);
|
|
251
|
+
public static readonly IntPtr DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = new(-4);
|
|
252
|
+
|
|
253
|
+
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
254
|
+
|
|
255
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
256
|
+
public struct RECT
|
|
257
|
+
{
|
|
258
|
+
public int Left;
|
|
259
|
+
public int Top;
|
|
260
|
+
public int Right;
|
|
261
|
+
public int Bottom;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
265
|
+
public struct PROCESSENTRY32W
|
|
266
|
+
{
|
|
267
|
+
public uint dwSize;
|
|
268
|
+
public uint cntUsage;
|
|
269
|
+
public uint th32ProcessID;
|
|
270
|
+
public IntPtr th32DefaultHeapID;
|
|
271
|
+
public uint th32ModuleID;
|
|
272
|
+
public uint cntThreads;
|
|
273
|
+
public uint th32ParentProcessID;
|
|
274
|
+
public int pcPriClassBase;
|
|
275
|
+
public uint dwFlags;
|
|
276
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
|
|
277
|
+
public string szExeFile;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
[DllImport("user32.dll")]
|
|
281
|
+
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
|
282
|
+
|
|
283
|
+
[DllImport("user32.dll")]
|
|
284
|
+
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
|
285
|
+
|
|
286
|
+
[DllImport("user32.dll")]
|
|
287
|
+
public static extern bool IsWindowVisible(IntPtr hWnd);
|
|
288
|
+
|
|
289
|
+
[DllImport("user32.dll")]
|
|
290
|
+
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
|
291
|
+
|
|
292
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
|
293
|
+
public static extern int GetWindowTextW(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
|
294
|
+
|
|
295
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
|
296
|
+
public static extern int GetWindowTextLengthW(IntPtr hWnd);
|
|
297
|
+
|
|
298
|
+
[DllImport("user32.dll")]
|
|
299
|
+
public static extern bool SetProcessDpiAwarenessContext(IntPtr value);
|
|
300
|
+
|
|
301
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
302
|
+
public static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);
|
|
303
|
+
|
|
304
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
|
305
|
+
public static extern bool Process32FirstW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
|
|
306
|
+
|
|
307
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
|
308
|
+
public static extern bool Process32NextW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
|
|
309
|
+
|
|
310
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
311
|
+
public static extern bool CloseHandle(IntPtr hObject);
|
|
312
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Windows Window Helper
|
|
2
|
+
|
|
3
|
+
Strategy id: `win32-window-helper` (best-effort).
|
|
4
|
+
|
|
5
|
+
Implements the shared helper protocol in [`../README.md`](../README.md).
|
|
6
|
+
|
|
7
|
+
## Responsibility
|
|
8
|
+
|
|
9
|
+
- Walk from the AI client PID up the parent process tree to the hosting terminal
|
|
10
|
+
process (`WindowsTerminal.exe`, `powershell.exe`, `pwsh.exe`, `cmd.exe`, `Code.exe`).
|
|
11
|
+
- Enumerate top-level windows with `EnumWindows`, match the owning PID with
|
|
12
|
+
`GetWindowThreadProcessId`, and read bounds with `GetWindowRect`.
|
|
13
|
+
- Return window bounds for bubble attachment.
|
|
14
|
+
|
|
15
|
+
## Implementation notes
|
|
16
|
+
|
|
17
|
+
- Suggested language: C# (.NET) or C++ with the Win32 API.
|
|
18
|
+
- Use per-monitor DPI awareness (`SetProcessDpiAwarenessContext`) so reported
|
|
19
|
+
bounds are physical pixels the runtime can convert.
|
|
20
|
+
- Windows Terminal tab/pane precision is not reliable; return the window rect and
|
|
21
|
+
let the runtime attach to a corner. Report `confidence` accordingly.
|
|
22
|
+
- VS Code integrated terminal pane precision requires a VS Code extension; the
|
|
23
|
+
helper should return the editor window rect with lower `confidence`.
|
|
24
|
+
|
|
25
|
+
## Protocol mapping
|
|
26
|
+
|
|
27
|
+
- `op: "capabilities"` → `{ "locate": true, "follow": false, "permission": "granted" }`.
|
|
28
|
+
- `op: "locate"` → `window` rect on success, or `{ "ok": false, "error": "not_found" }`.
|
|
29
|
+
|
|
30
|
+
## Status: implemented
|
|
31
|
+
|
|
32
|
+
This helper is implemented in C# (.NET, `Program.cs`) and builds with the .NET
|
|
33
|
+
SDK. It walks the process tree with Toolhelp (`CreateToolhelp32Snapshot`),
|
|
34
|
+
enumerates top-level windows (`EnumWindows` + `GetWindowThreadProcessId` +
|
|
35
|
+
`GetWindowRect`), prefers known terminal processes, and is per-monitor DPI aware.
|
|
36
|
+
|
|
37
|
+
### Build
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd native/win-window-helper
|
|
41
|
+
dotnet build -c Release
|
|
42
|
+
# -> bin/Release/net10.0-windows/haya-pet-win-window-helper.exe
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Try it
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
echo {"id":"a","op":"capabilities"} | bin/Release/net10.0-windows/haya-pet-win-window-helper.exe
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The `apps/companion/src/main/terminal-helper-client.js` client spawns this exe
|
|
52
|
+
and speaks the protocol; the companion converts the returned window bounds to the
|
|
53
|
+
pet's display/DPI space via `display-manager.js`.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
2
|
+
|
|
3
|
+
<PropertyGroup>
|
|
4
|
+
<OutputType>Exe</OutputType>
|
|
5
|
+
<TargetFramework>net10.0-windows</TargetFramework>
|
|
6
|
+
<Nullable>enable</Nullable>
|
|
7
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
8
|
+
<AssemblyName>haya-pet-win-window-helper</AssemblyName>
|
|
9
|
+
<InvariantGlobalization>true</InvariantGlobalization>
|
|
10
|
+
</PropertyGroup>
|
|
11
|
+
|
|
12
|
+
</Project>
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hayasaka7/haya-pet",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Generic AI CLI pet runtime foundation.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Ai Hayasaka",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/HAYASAKA7/HAYA-PET.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/HAYASAKA7/HAYA-PET#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/HAYASAKA7/HAYA-PET/issues"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"haya-pet": "apps/cli/src/haya-pet.js"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node test/run-tests.mjs"
|
|
21
|
+
},
|
|
22
|
+
"workspaces": [
|
|
23
|
+
"apps/*",
|
|
24
|
+
"packages/*"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"electron": "42.3.3"
|
|
28
|
+
},
|
|
29
|
+
"optionalDependencies": {
|
|
30
|
+
"node-pty": "^1.1.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=16.20.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const ADAPTERS = Object.freeze({
|
|
2
|
+
codex: Object.freeze({
|
|
3
|
+
id: "codex",
|
|
4
|
+
displayName: "Codex",
|
|
5
|
+
detection: "manual",
|
|
6
|
+
supportLevel: 2,
|
|
7
|
+
defaultCommand: "codex",
|
|
8
|
+
knownProcessNames: Object.freeze(["codex"]),
|
|
9
|
+
stateHeuristics: Object.freeze(["pty_output"])
|
|
10
|
+
}),
|
|
11
|
+
"claude-code": Object.freeze({
|
|
12
|
+
id: "claude-code",
|
|
13
|
+
displayName: "Claude Code",
|
|
14
|
+
detection: "manual",
|
|
15
|
+
supportLevel: 2,
|
|
16
|
+
defaultCommand: "claude",
|
|
17
|
+
knownProcessNames: Object.freeze(["claude"]),
|
|
18
|
+
stateHeuristics: Object.freeze(["pty_output", "official_plugin"])
|
|
19
|
+
}),
|
|
20
|
+
antigravity: Object.freeze({
|
|
21
|
+
id: "antigravity",
|
|
22
|
+
displayName: "Antigravity",
|
|
23
|
+
detection: "manual",
|
|
24
|
+
supportLevel: 1,
|
|
25
|
+
defaultCommand: "antigravity",
|
|
26
|
+
knownProcessNames: Object.freeze(["antigravity"]),
|
|
27
|
+
stateHeuristics: Object.freeze(["wrapper"])
|
|
28
|
+
}),
|
|
29
|
+
generic: Object.freeze({
|
|
30
|
+
id: "generic",
|
|
31
|
+
displayName: "Generic",
|
|
32
|
+
detection: "manual",
|
|
33
|
+
supportLevel: 1,
|
|
34
|
+
defaultCommand: undefined,
|
|
35
|
+
knownProcessNames: Object.freeze([]),
|
|
36
|
+
stateHeuristics: Object.freeze(["wrapper"])
|
|
37
|
+
})
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const KNOWN_CLIENT_IDS = Object.freeze(["codex", "claude-code", "antigravity", "generic"]);
|
|
41
|
+
|
|
42
|
+
export function listAdapters() {
|
|
43
|
+
return KNOWN_CLIENT_IDS.map((id) => ADAPTERS[id]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getAdapterInfo(clientId) {
|
|
47
|
+
return ADAPTERS[clientId];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveAdapterInfo(clientId) {
|
|
51
|
+
const known = ADAPTERS[clientId];
|
|
52
|
+
if (known) {
|
|
53
|
+
return known;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
...ADAPTERS.generic,
|
|
58
|
+
id: clientId,
|
|
59
|
+
displayName: clientId
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Adapter capability declarations (product plan section 23). The talk window
|
|
2
|
+
// must hide or disable any control an adapter cannot safely support.
|
|
3
|
+
|
|
4
|
+
const WRAPPER_ONLY = Object.freeze({
|
|
5
|
+
canReply: "unsupported",
|
|
6
|
+
canApprove: "unsupported",
|
|
7
|
+
canPause: false,
|
|
8
|
+
canResume: false,
|
|
9
|
+
canStop: true,
|
|
10
|
+
canFocusTerminal: true,
|
|
11
|
+
canOpenTranscript: false,
|
|
12
|
+
canShowDiffs: false,
|
|
13
|
+
canShowFiles: false,
|
|
14
|
+
canShowTests: false
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const PTY_OBSERVER = Object.freeze({
|
|
18
|
+
canReply: "best_effort",
|
|
19
|
+
canApprove: "best_effort",
|
|
20
|
+
canPause: false,
|
|
21
|
+
canResume: false,
|
|
22
|
+
canStop: true,
|
|
23
|
+
canFocusTerminal: true,
|
|
24
|
+
canOpenTranscript: false,
|
|
25
|
+
canShowDiffs: false,
|
|
26
|
+
canShowFiles: false,
|
|
27
|
+
canShowTests: false
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const ADAPTER_CAPABILITIES = Object.freeze({
|
|
31
|
+
generic: WRAPPER_ONLY,
|
|
32
|
+
antigravity: WRAPPER_ONLY,
|
|
33
|
+
codex: PTY_OBSERVER,
|
|
34
|
+
"claude-code": PTY_OBSERVER
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export function getAdapterCapabilities(clientId) {
|
|
38
|
+
return ADAPTER_CAPABILITIES[clientId] ?? WRAPPER_ONLY;
|
|
39
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { isAiClientState } from "../../protocol/src/messages.js";
|
|
2
|
+
|
|
3
|
+
// Generic regex rules from the product plan (section 35). Patterns are matched
|
|
4
|
+
// case-insensitively against a single line of client output.
|
|
5
|
+
export const DEFAULT_GENERIC_RULES = Object.freeze({
|
|
6
|
+
waiting_approval: ["approve", "permission", "continue\\?", "allow"],
|
|
7
|
+
waiting_user: ["\\? for", "press enter", "type your", "your input"],
|
|
8
|
+
running_tool: ["running", "executing", "\\$ "],
|
|
9
|
+
failed: ["error", "failed", "exception", "traceback"],
|
|
10
|
+
reviewing: ["review", "diff", "summary"]
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const CLIENT_RULES = Object.freeze({
|
|
14
|
+
codex: Object.freeze({
|
|
15
|
+
...DEFAULT_GENERIC_RULES,
|
|
16
|
+
editing_files: ["applying patch", "editing", "writing file", "apply_patch"],
|
|
17
|
+
waiting_approval: ["approve", "permission", "allow command", "run command\\?"]
|
|
18
|
+
}),
|
|
19
|
+
"claude-code": Object.freeze({
|
|
20
|
+
...DEFAULT_GENERIC_RULES,
|
|
21
|
+
editing_files: ["edit ", "writing to", "applying edit"],
|
|
22
|
+
running_tool: ["running", "executing", "bash", "\\$ "],
|
|
23
|
+
waiting_approval: ["permission", "do you want to proceed", "allow"],
|
|
24
|
+
waiting_user: ["press enter", "your input", "human:"]
|
|
25
|
+
}),
|
|
26
|
+
antigravity: Object.freeze({
|
|
27
|
+
...DEFAULT_GENERIC_RULES,
|
|
28
|
+
thinking: ["generating", "thinking"]
|
|
29
|
+
})
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Most-urgent-first precedence so a line matching several categories resolves
|
|
33
|
+
// to the state the user most needs to see (mirrors the session priority model).
|
|
34
|
+
const MATCH_PRECEDENCE = Object.freeze([
|
|
35
|
+
"waiting_approval",
|
|
36
|
+
"waiting_user",
|
|
37
|
+
"failed",
|
|
38
|
+
"editing_files",
|
|
39
|
+
"running_tool",
|
|
40
|
+
"reviewing",
|
|
41
|
+
"compacting",
|
|
42
|
+
"thinking",
|
|
43
|
+
"success",
|
|
44
|
+
"idle"
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export function getClientRules(clientId) {
|
|
48
|
+
return CLIENT_RULES[clientId] ?? DEFAULT_GENERIC_RULES;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function matchAiState(text, rules = DEFAULT_GENERIC_RULES) {
|
|
52
|
+
if (typeof text !== "string" || text === "") {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const compiled = compileRules(rules);
|
|
57
|
+
|
|
58
|
+
for (const state of MATCH_PRECEDENCE) {
|
|
59
|
+
const regexes = compiled[state];
|
|
60
|
+
if (!regexes) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (regexes.some((regex) => regex.test(text))) {
|
|
65
|
+
return state;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function compileRules(rules) {
|
|
73
|
+
const compiled = {};
|
|
74
|
+
|
|
75
|
+
if (!isPlainObject(rules)) {
|
|
76
|
+
return compiled;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const [state, patterns] of Object.entries(rules)) {
|
|
80
|
+
if (!isAiClientState(state) || !Array.isArray(patterns)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const regexes = [];
|
|
85
|
+
for (const pattern of patterns) {
|
|
86
|
+
const regex = safeCompile(pattern);
|
|
87
|
+
if (regex) {
|
|
88
|
+
regexes.push(regex);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (regexes.length > 0) {
|
|
93
|
+
compiled[state] = regexes;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return compiled;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function safeCompile(pattern) {
|
|
101
|
+
if (typeof pattern !== "string") {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
return new RegExp(pattern, "i");
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isPlainObject(value) {
|
|
113
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
114
|
+
}
|