feat: v2.5.4 — Query Explorer folded into Sessions, Windows picker and paths
- Sessions page tabs: Sessions · Events · Agents. QueryExplorer.tsx became AgentsTable.tsx (agents only — sessions and events already had tabs); /query redirects to /sessions?view=agents and leaves the sidebar. GET /api/query is unchanged and still answers all three entities. - Windows picker opens in front: -EncodedCommand script spends the hidden first ShowWindow that windowsHide gives the process, then brings the TopMost owner forward via AttachThreadInput (v2.5.3's owner was itself hidden). - Session page Resume/Fork/Open pane: owningLane and sessionWorktreeName match Windows folders (either separator), with a test.
This commit is contained in:
+1
-1
@@ -463,7 +463,7 @@ The OpenAPI spec is generated from `server/openapi.js` (`createOpenApiSpec()`),
|
||||
| `POST` | `/api/snapshots` | `{session_id, redact?: {file_paths?}}` → freeze the session into an immutable row under a random token (same-origin) |
|
||||
| `GET` | `/api/snapshots/:token` | `{snapshot, payload}` — the frozen session, agents, events, stats and cost |
|
||||
| `DELETE`| `/api/snapshots/:id` | Delete a snapshot (same-origin) |
|
||||
| `GET` | `/api/query` | Query Explorer: sessions / events / agents filtered, sorted from an allowlist, paged; `format=csv\|json` exports the whole filter (≤ 50,000 rows) — `server/routes/query.js` |
|
||||
| `GET` | `/api/query` | Query Explorer (the Sessions page's Agents tab): sessions / events / agents filtered, sorted from an allowlist, paged; `format=csv\|json` exports the whole filter (≤ 50,000 rows) — `server/routes/query.js` |
|
||||
| `GET` | `/api/reports` | Scheduled report definitions with their latest run (`server/lib/reports.js`) |
|
||||
| `POST`/`PUT`/`DELETE` | `/api/reports[/:id]` | Create / update / delete a report definition (same-origin; delete also removes its runs and files) |
|
||||
| `POST` | `/api/reports/:id/run` | Run a report now (same-origin) |
|
||||
|
||||
@@ -44,12 +44,17 @@ describe("native-dialog", () => {
|
||||
assert.ok(!args.slice(0, -2).join(" ").includes("shell script"));
|
||||
});
|
||||
|
||||
it("gives the Windows dialog a TopMost owner, so it does not open behind the browser", () => {
|
||||
it("gives the Windows dialog a foreground TopMost owner, so it does not open behind the browser", () => {
|
||||
for (const kind of ["dir", "file"]) {
|
||||
const [[cmd, args]] = nd.dialogCommands(kind, "C:\\x", "win32");
|
||||
assert.equal(cmd, "powershell.exe");
|
||||
const script = args.at(-1);
|
||||
assert.match(script, /\$o\.TopMost = \$true;.*\$o\.Show\(\);.*\$d\.ShowDialog\(\$o\)/);
|
||||
assert.equal(args.at(-2), "-EncodedCommand");
|
||||
const script = Buffer.from(args.at(-1), "base64").toString("utf16le");
|
||||
assert.match(
|
||||
script,
|
||||
/\$o\.TopMost = \$true;[\s\S]*ShowWindow\(\$o\.Handle, 1\)[\s\S]*\$o\.Show\(\)[\s\S]*Front\(\$o\.Handle\)[\s\S]*\$d\.ShowDialog\(\$o\)/
|
||||
);
|
||||
assert.match(script, /AttachThreadInput/);
|
||||
assert.ok(!script.includes("C:\\x"), "the folder travels in env, never the script");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -61,6 +61,25 @@ function startDir(start) {
|
||||
return os.homedir();
|
||||
}
|
||||
|
||||
// user32 calls PowerShell has no cmdlet for: `Front` makes a window the
|
||||
// foreground one from a background process (see dialogCommands).
|
||||
const WIN_FRONT = [
|
||||
'[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);',
|
||||
'[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);',
|
||||
'[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);',
|
||||
'[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();',
|
||||
'[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, IntPtr p);',
|
||||
'[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool on);',
|
||||
'[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();',
|
||||
"public static void Front(IntPtr h) {",
|
||||
" uint fg = GetWindowThreadProcessId(GetForegroundWindow(), IntPtr.Zero);",
|
||||
" uint me = GetCurrentThreadId();",
|
||||
" bool joined = fg != 0 && fg != me && AttachThreadInput(me, fg, true);",
|
||||
" BringWindowToTop(h); SetForegroundWindow(h);",
|
||||
" if (joined) AttachThreadInput(me, fg, false);",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
/** The candidate dialog commands for a platform, in order of preference. */
|
||||
function dialogCommands(kind, dir, platform = process.platform) {
|
||||
const title = TITLES[kind];
|
||||
@@ -88,19 +107,36 @@ function dialogCommands(kind, dir, platform = process.platform) {
|
||||
? "$d = New-Object System.Windows.Forms.FolderBrowserDialog; $d.SelectedPath = $env:AGENTHUB_PICK_START; $d.Description = $env:AGENTHUB_PICK_TITLE"
|
||||
: "$d = New-Object System.Windows.Forms.OpenFileDialog; $d.InitialDirectory = $env:AGENTHUB_PICK_START; $d.Title = $env:AGENTHUB_PICK_TITLE";
|
||||
const result = kind === "dir" ? "$d.SelectedPath" : "$d.FileName";
|
||||
// The server is a background process, and Windows keeps one from bringing
|
||||
// a window forward: an ownerless dialog opened at the BOTTOM of the
|
||||
// z-order, behind the browser. So the dialog gets a TopMost owner, made
|
||||
// the foreground window by joining the current foreground thread's input
|
||||
// (AttachThreadInput — the lock exempts it). The first ShowWindow of a
|
||||
// process started with windowsHide is turned into a hide; a throwaway call
|
||||
// spends it, or the owner stays hidden and takes the dialog down with it.
|
||||
const script = [
|
||||
"$ProgressPreference = 'SilentlyContinue'",
|
||||
"[Console]::OutputEncoding = [Text.Encoding]::UTF8",
|
||||
"Add-Type -AssemblyName System.Windows.Forms",
|
||||
`Add-Type -Namespace AgentHub -Name Win -MemberDefinition '${WIN_FRONT}'`,
|
||||
"$o = New-Object System.Windows.Forms.Form",
|
||||
"$o.TopMost = $true; $o.ShowInTaskbar = $false; $o.FormBorderStyle = 'None'; $o.Opacity = 0; $o.Width = 1; $o.Height = 1; $o.StartPosition = 'CenterScreen'",
|
||||
"[void][AgentHub.Win]::ShowWindow($o.Handle, 1)",
|
||||
"$o.Show()",
|
||||
"[AgentHub.Win]::Front($o.Handle)",
|
||||
dialog,
|
||||
`if ($d.ShowDialog($o) -eq 'OK') { [Console]::Out.Write(${result}) } else { exit 1 }`,
|
||||
].join("\n");
|
||||
// -EncodedCommand: the C# above carries double quotes, which Windows
|
||||
// PowerShell's own command-line parsing mangles inside -Command.
|
||||
return [
|
||||
[
|
||||
"powershell.exe",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-STA",
|
||||
"-Command",
|
||||
// The server is a background process, and Windows will not let one
|
||||
// bring a window to the front: an ownerless dialog opened BEHIND the
|
||||
// browser (or hidden — the first ShowWindow of a process started
|
||||
// with windowsHide). An invisible TopMost owner, shown first, takes
|
||||
// that first ShowWindow and keeps the dialog on top.
|
||||
`[Console]::OutputEncoding = [Text.Encoding]::UTF8; Add-Type -AssemblyName System.Windows.Forms; $o = New-Object System.Windows.Forms.Form; $o.TopMost = $true; $o.ShowInTaskbar = $false; $o.FormBorderStyle = 'None'; $o.Opacity = 0; $o.Width = 1; $o.Height = 1; $o.Show(); $o.Activate(); ${dialog}; if ($d.ShowDialog($o) -eq 'OK') { [Console]::Out.Write(${result}) } else { exit 1 }`,
|
||||
"-EncodedCommand",
|
||||
Buffer.from(script, "utf16le").toString("base64"),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user