summrise-agent 1.2.489 → 1.2.491
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/bin/summrise.js +145 -3
- package/package.json +1 -1
- package/summrise-agent.exe +0 -0
- package/summrise-desktop-electron/src/main.js +93 -64
package/bin/summrise.js
CHANGED
|
@@ -34,8 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
-
exports.BOOT_TASKS = exports.psq = void 0;
|
|
37
|
+
exports.BOOT_TASKS = exports.DESKTOP_AUMID = exports.psq = void 0;
|
|
38
38
|
exports.psArgv = psArgv;
|
|
39
|
+
exports.lnkIdentity = lnkIdentity;
|
|
39
40
|
exports.deskShortcutRepairPs = deskShortcutRepairPs;
|
|
40
41
|
exports.startDesktopPs = startDesktopPs;
|
|
41
42
|
exports.desktopTaskPs = desktopTaskPs;
|
|
@@ -298,13 +299,135 @@ function psFile(script) {
|
|
|
298
299
|
// exported: unit-tested in test/cli.test.mjs (SYSTEM-context PS quoting = injection surface)
|
|
299
300
|
const psq = (x) => String(x).replace(/'/g, "''");
|
|
300
301
|
exports.psq = psq;
|
|
302
|
+
/** The taskbar identity: ONE string, and it lives on BOTH sides of an association.
|
|
303
|
+
*
|
|
304
|
+
* It is the AppUserModelID the desktop shell sets on its process
|
|
305
|
+
* (`agent/summrise-desktop-electron/src/main.ts`) and the value written into
|
|
306
|
+
* `System.AppUserModel.ID` on the shortcut below. Microsoft's lookup for the taskbar
|
|
307
|
+
* group's icon takes the shortcut arm only when the window carries an explicit ID, and
|
|
308
|
+
* the window's arm only names a shortcut carrying the SAME ID — so the two halves are one
|
|
309
|
+
* association and this is the string that ties them. If they ever disagree, the icon
|
|
310
|
+
* silently reverts to the backing executable (stock `electron.exe`, since the shell runs
|
|
311
|
+
* unpackaged) with nothing on the device to read that says why.
|
|
312
|
+
* PowerShell/Windows AppUserModelIDs cannot contain spaces, and this one does not.
|
|
313
|
+
*/
|
|
314
|
+
exports.DESKTOP_AUMID = "online.saisi.summrise.agent";
|
|
315
|
+
/** The two `IPropertyStore` values `Summrise.lnk` must carry, as DATA.
|
|
316
|
+
*
|
|
317
|
+
* Deliberately separated from the PowerShell that performs the write, and exported, so the
|
|
318
|
+
* DECISION is testable where the COM call is not: a test can pin what the CLI intends to
|
|
319
|
+
* write without a Windows box. `icoPath` is the shell's own sunrise `.ico`; the `,0` is the
|
|
320
|
+
* icon index and is REQUIRED — the property's own documentation: "because .ico files can
|
|
321
|
+
* contain multiple icon resources, a resource ID is required in the string. If the .ico
|
|
322
|
+
* file is a single image, use '0'".
|
|
323
|
+
*
|
|
324
|
+
* `IconLocation` on the shortcut is a DIFFERENT thing and is left alone: it is what
|
|
325
|
+
* Explorer draws for the .lnk FILE. These two are what the taskbar GROUP uses.
|
|
326
|
+
*/
|
|
327
|
+
function lnkIdentity(icoPath) {
|
|
328
|
+
return { id: exports.DESKTOP_AUMID, icon: `${icoPath},0` };
|
|
329
|
+
}
|
|
330
|
+
/** C# interop for the two shortcut properties `WScript.Shell` cannot set.
|
|
331
|
+
*
|
|
332
|
+
* WHY THIS MECHANISM, given this repository prefers Rust for logic: `System.AppUserModel.ID`
|
|
333
|
+
* and `System.AppUserModel.RelaunchIconResource` are not fields of the .lnk header — they
|
|
334
|
+
* live in the shortcut's `IPropertyStore`, and `WScript.Shell` exposes no way to reach one.
|
|
335
|
+
* The route Microsoft documents for a shortcut is `IShellLink` + `QueryInterface` for
|
|
336
|
+
* `IPropertyStore` ("A shortcut (as an IShellLink, CLSID_ShellLink, or a .lnk file) supports
|
|
337
|
+
* properties through IPropertyStore", shell/appids.md) — COM interop, in the launcher's own
|
|
338
|
+
* script generator, which is already PowerShell and already writing this shortcut.
|
|
339
|
+
* `Add-Type` compiles it with the C# compiler that ships IN the box: nothing installed,
|
|
340
|
+
* nothing downloaded, and it runs on a stock Windows box with no Node, no Rust and no SDK.
|
|
341
|
+
* A Rust helper is the repository's usual preference and was rejected HERE on cost, not on
|
|
342
|
+
* taste: it needs a new cross-compiled binary in the npm package, and this package "carries
|
|
343
|
+
* NO boxed components" by design — `setup` fetches and hash-pins them — so ~60 lines of
|
|
344
|
+
* interop would buy a build stage, a release-host component and a new pin.
|
|
345
|
+
*
|
|
346
|
+
* TWO THINGS THE DEVICE TAUGHT, both load-bearing:
|
|
347
|
+
* - `IPersistFile::Load` must be given STGM_READWRITE (2). Loading with 0 opens the link
|
|
348
|
+
* READ-ONLY and the later `IPropertyStore::Commit` fails STG_E_ACCESSDENIED (0x80030005)
|
|
349
|
+
* — measured on desktop-14rjcr8, where the first version of this passed 0 and wrote
|
|
350
|
+
* nothing while reporting no error the caller could see.
|
|
351
|
+
* - the write is verified by READING THE PROPERTIES BACK with `Read`, which is also what
|
|
352
|
+
* decides whether the shortcut needs repairing at all.
|
|
353
|
+
*/
|
|
354
|
+
const LNK_PROPS_CS = `using System;
|
|
355
|
+
using System.Runtime.InteropServices;
|
|
356
|
+
public static class SummriseLnk {
|
|
357
|
+
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
|
358
|
+
struct PROPERTYKEY { public Guid fmtid; public uint pid; }
|
|
359
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
360
|
+
struct PROPVARIANT { public ushort vt, r1, r2, r3; public IntPtr p, p2; }
|
|
361
|
+
[ComImport, Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
362
|
+
interface IPropertyStore {
|
|
363
|
+
int GetCount(out uint c);
|
|
364
|
+
int GetAt(uint i, out PROPERTYKEY k);
|
|
365
|
+
int GetValue(ref PROPERTYKEY k, out PROPVARIANT v);
|
|
366
|
+
int SetValue(ref PROPERTYKEY k, ref PROPVARIANT v);
|
|
367
|
+
int Commit();
|
|
368
|
+
}
|
|
369
|
+
[ComImport, Guid("0000010b-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
370
|
+
interface IPersistFile {
|
|
371
|
+
void GetClassID(out Guid g);
|
|
372
|
+
[PreserveSig] int IsDirty();
|
|
373
|
+
void Load([MarshalAs(UnmanagedType.LPWStr)] string f, uint m);
|
|
374
|
+
void Save([MarshalAs(UnmanagedType.LPWStr)] string f, [MarshalAs(UnmanagedType.Bool)] bool r);
|
|
375
|
+
void SaveCompleted([MarshalAs(UnmanagedType.LPWStr)] string f);
|
|
376
|
+
void GetCurFile([MarshalAs(UnmanagedType.LPWStr)] out string f);
|
|
377
|
+
}
|
|
378
|
+
static readonly Guid CLSID = new Guid("00021401-0000-0000-C000-000000000046");
|
|
379
|
+
static readonly Guid FMT = new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3");
|
|
380
|
+
const uint RW = 2;
|
|
381
|
+
static PROPERTYKEY K(uint pid) { PROPERTYKEY k; k.fmtid = FMT; k.pid = pid; return k; }
|
|
382
|
+
static object New(string lnk, uint mode) {
|
|
383
|
+
object o = Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID));
|
|
384
|
+
((IPersistFile)o).Load(lnk, mode);
|
|
385
|
+
return o;
|
|
386
|
+
}
|
|
387
|
+
static string Get(string lnk, uint pid, uint mode) {
|
|
388
|
+
IPropertyStore s = (IPropertyStore)New(lnk, mode);
|
|
389
|
+
PROPERTYKEY k = K(pid); PROPVARIANT v;
|
|
390
|
+
if (s.GetValue(ref k, out v) != 0) return "";
|
|
391
|
+
if (v.vt != 31 || v.p == IntPtr.Zero) return "";
|
|
392
|
+
string r = Marshal.PtrToStringUni(v.p);
|
|
393
|
+
Marshal.FreeCoTaskMem(v.p);
|
|
394
|
+
return r;
|
|
395
|
+
}
|
|
396
|
+
static void Put(IPropertyStore s, uint pid, string val) {
|
|
397
|
+
PROPERTYKEY k = K(pid); PROPVARIANT v = new PROPVARIANT();
|
|
398
|
+
v.vt = 31; v.p = Marshal.StringToCoTaskMemUni(val);
|
|
399
|
+
try { int hr = s.SetValue(ref k, ref v); if (hr != 0) Marshal.ThrowExceptionForHR(hr); }
|
|
400
|
+
finally { Marshal.FreeCoTaskMem(v.p); }
|
|
401
|
+
}
|
|
402
|
+
public static string Read(string lnk) { return Get(lnk, 5, 0) + "|" + Get(lnk, 3, 0); }
|
|
403
|
+
public static void Write(string lnk, string id, string icon) {
|
|
404
|
+
object o = New(lnk, RW);
|
|
405
|
+
IPropertyStore s = (IPropertyStore)o;
|
|
406
|
+
Put(s, 5, id); Put(s, 3, icon);
|
|
407
|
+
int hr = s.Commit();
|
|
408
|
+
if (hr != 0) Marshal.ThrowExceptionForHR(hr);
|
|
409
|
+
((IPersistFile)o).Save(lnk, true);
|
|
410
|
+
}
|
|
411
|
+
}`;
|
|
301
412
|
// stage-brand: healed desktop shortcut. A 2026-09-01 Summrise.lnk on devices
|
|
302
413
|
// points at the RETIRED Tauri summrise-desktop.exe (embedded stale icon) —
|
|
303
414
|
// double-clicking it launches the dead app instead of the Electron shell.
|
|
304
415
|
// Repair (only if the link already exists — headless installs must not
|
|
305
416
|
// sprout desktop icons): repoint at start-desktop.ps1 (the SummriseDesktop
|
|
306
|
-
// onlogon path) with IconLocation pinned to the sunrise icon.ico,
|
|
307
|
-
//
|
|
417
|
+
// onlogon path) with IconLocation pinned to the sunrise icon.ico, remove the
|
|
418
|
+
// retired Tauri/tray orphans nothing ships anymore, AND write the taskbar
|
|
419
|
+
// identity (System.AppUserModel.ID + RelaunchIconResource) into the link's
|
|
420
|
+
// property store.
|
|
421
|
+
//
|
|
422
|
+
// WHY THE IDENTITY IS PART OF *THIS* FUNCTION: a shortcut with the right target
|
|
423
|
+
// and the wrong (or absent) identity is exactly the state that draws the
|
|
424
|
+
// Electron logo — the shell sets the same AppUserModelID on its process, and the
|
|
425
|
+
// taskbar's icon lookup reaches the shortcut arm only when a shortcut carrying
|
|
426
|
+
// that ID can be found. MEASURED on desktop-14rjcr8 before this change: both
|
|
427
|
+
// Summrise.lnk files existed and carried NEITHER property, which is why an
|
|
428
|
+
// explicit ID on the window alone changed nothing and why deleting that ID
|
|
429
|
+
// (d4edbb64) changed nothing either.
|
|
430
|
+
//
|
|
308
431
|
// Best-effort + logged, never fatal. `sink` is a PS output pipe
|
|
309
432
|
// (e.g. Write-Host, or the update log pipe). Single-quoted PS literals
|
|
310
433
|
// only (npm audit #6) except the double quotes the .lnk Arguments path
|
|
@@ -313,18 +436,37 @@ exports.psq = psq;
|
|
|
313
436
|
// script to `ps()` as argv and the update swap runs it from a file. The
|
|
314
437
|
// backslash-escape `setup` used to apply was deleted with that conversion
|
|
315
438
|
// (see step 7), which is why this line no longer says "escape them".
|
|
439
|
+
// The C# rides in a single-quoted HERE-STRING (`@'` … `'@`, both alone on
|
|
440
|
+
// their line): it contains double quotes and is literal end to end, so the
|
|
441
|
+
// doubling rule above cannot be defeated by anything inside it.
|
|
316
442
|
// exported: unit-tested in test/cli.test.mjs.
|
|
317
443
|
function deskShortcutRepairPs(scriptsQ, deskDirQ, sink) {
|
|
444
|
+
const { id, icon } = lnkIdentity(`${deskDirQ}\\icon.ico`);
|
|
318
445
|
return [
|
|
319
446
|
`$dLnk = Join-Path $env:PUBLIC 'Desktop\\Summrise.lnk'`,
|
|
320
447
|
`$dIco = '${deskDirQ}\\icon.ico'`,
|
|
321
448
|
`$dPs1 = '${scriptsQ}\\start-desktop.ps1'`,
|
|
449
|
+
`$dId = '${id}'`,
|
|
450
|
+
`$dRes = '${icon}'`,
|
|
451
|
+
`$dSet = $false`,
|
|
322
452
|
`$dNeed = $false`,
|
|
323
453
|
`if (Test-Path $dLnk) {`,
|
|
454
|
+
// Compiled only when there IS a link to write, so a headless install does not pay for
|
|
455
|
+
// it. The `-as [type]` guard keeps a second call in one process from turning a
|
|
456
|
+
// "type already exists" throw into a silent skip of the write below.
|
|
457
|
+
` $dCs = @'\n${LNK_PROPS_CS}\n'@`,
|
|
458
|
+
` try { if (-not ('SummriseLnk' -as [type])) { Add-Type -TypeDefinition $dCs }; $dSet = [bool]('SummriseLnk' -as [type]) } catch { ('desk: lnk identity helper unavailable: ' + $_.Exception.Message) | ${sink} }`,
|
|
324
459
|
` try { $dEx = (New-Object -ComObject WScript.Shell).CreateShortcut($dLnk); if (($dEx.TargetPath -like '*summrise-desktop.exe') -or ($dEx.TargetPath -like '*summrise-tray.exe') -or (-not (Test-Path $dEx.TargetPath))) { $dNeed = $true } } catch { $dNeed = $true }`,
|
|
460
|
+
// ...and a link that is otherwise healthy still needs repairing when its identity is
|
|
461
|
+
// missing or names a different ID/icon. `Read` returns "id|icon", so ONE comparison
|
|
462
|
+
// covers both properties — and it is the same call the verification below prints.
|
|
463
|
+
` if ($dSet) { try { if (([SummriseLnk]::Read($dLnk)) -ne ($dId + '|' + $dRes)) { $dNeed = $true } } catch { $dNeed = $true } }`,
|
|
325
464
|
`}`,
|
|
326
465
|
`if ($dNeed -and (Test-Path $dIco) -and (Test-Path $dPs1)) {`,
|
|
327
466
|
` try { $dWs = New-Object -ComObject WScript.Shell; $dSc = $dWs.CreateShortcut($dLnk); $dSc.TargetPath = Join-Path $env:SystemRoot 'System32\\WindowsPowerShell\\v1.0\\powershell.exe'; $dSc.Arguments = '-NoProfile -ExecutionPolicy Bypass -File "' + $dPs1 + '"'; $dSc.WorkingDirectory = '${deskDirQ}'; $dSc.IconLocation = $dIco + ',0'; $dSc.Save(); 'desk: Summrise.lnk repointed to electron shell' | ${sink} } catch { ('desk: Summrise.lnk repair failed: ' + $_.Exception.Message) | ${sink} }`,
|
|
467
|
+
// The verification is the READ-BACK, not the absence of an exception: SetValue+Commit
|
|
468
|
+
// returning cleanly is a claim about a call, and this records what the file now carries.
|
|
469
|
+
` if ($dSet) { try { [SummriseLnk]::Write($dLnk, $dId, $dRes); ('desk: Summrise.lnk taskbar identity [' + [SummriseLnk]::Read($dLnk) + ']') | ${sink} } catch { ('desk: Summrise.lnk identity write failed: ' + $_.Exception.Message) | ${sink} } }`,
|
|
328
470
|
`}`,
|
|
329
471
|
`foreach ($dRx in @('summrise-desktop.exe','summrise-tray.exe')) { $dRp = '${deskDirQ}\\' + $dRx; if (Test-Path $dRp) { try { Remove-Item -Force -ErrorAction Stop $dRp; ('desk: removed retired ' + $dRx) | ${sink} } catch { ('desk: retired ' + $dRx + ' locked, kept') | ${sink} } } }`,
|
|
330
472
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summrise-agent",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.491",
|
|
4
4
|
"description": "Summrise Agent — Windows device MCP server (terminal/SSH/serial) + panel. Self-hosted npm distribution.",
|
|
5
5
|
"//build": "唯一构建路径:在本目录执行 npm run build。它是自包含的:经 npm exec 按需拉取 typescript@5,无需预装任何 node_modules(干净检出可跑;旧脚本引用 ../summrise-desktop-electron/node_modules/.bin/tsc,干净检出必 ENOENT)。必须用 npm exec --package … -- tsc 的形式:npx 会吃掉传给 tsc 的 -p/--outDir 等参数(round-435:npx 写法从未真正跑通过)。先编 electron TS(--noCheck 纯 emit,与 release.yml 一致)同步两处 src/*.js,再编 src/summrise.ts 到 bin/summrise.js 并校验 round-298 标记(弱 oracle,只证某次构建发生,新鲜性由 publish-release.sh 的 tsc 重编 + cmp 门保证)。",
|
|
6
6
|
"bin": {
|
package/summrise-agent.exe
CHANGED
|
Binary file
|
|
@@ -47,6 +47,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
47
47
|
// No spawn() of summrise-agent.exe from JS — that was the d1 Chrome-OOM root
|
|
48
48
|
// cause (a bind-failed child wedged on "Press Enter to exit" forever).
|
|
49
49
|
const electron_1 = require("electron");
|
|
50
|
+
// IPC audit #6: execSync/ChildProcess were vestigial imports from the
|
|
51
|
+
// pre-async-spawn era (round-117 removed the call sites) — dropped so
|
|
52
|
+
// eslint/tsc keep the surface honest. The last one to go was the empty
|
|
53
|
+
// `import type {} from "child_process"` that stood here: it imported
|
|
54
|
+
// NOTHING (the schtasks runner reaches the module through require() with its
|
|
55
|
+
// own `as typeof import(...)` type), and a type-only import of a module the
|
|
56
|
+
// shell does not otherwise import is a "dependency" no reader can act on.
|
|
50
57
|
const path = __importStar(require("path"));
|
|
51
58
|
const fs = __importStar(require("fs"));
|
|
52
59
|
const http = __importStar(require("http"));
|
|
@@ -319,13 +326,7 @@ function buildMenu() {
|
|
|
319
326
|
{ label: "New Serial Connection…", accelerator: "CmdOrCtrl+Shift+P", click: () => sendMenu("new-serial") },
|
|
320
327
|
{ label: "New Browser Session…", accelerator: "CmdOrCtrl+Shift+B", click: () => sendMenu("new-browser") },
|
|
321
328
|
{ type: "separator" },
|
|
322
|
-
{ label: "Browser", accelerator: "CmdOrCtrl+Shift+G", click: () => sendMenu("open-browser") },
|
|
323
|
-
{ label: "Memory", accelerator: "CmdOrCtrl+Shift+M", click: () => sendMenu("open-memory") },
|
|
324
|
-
{ label: "Settings", accelerator: "CmdOrCtrl+Shift+,", click: () => sendMenu("open-settings") },
|
|
325
|
-
{ label: "Plugins", click: () => sendMenu("open-plugins") },
|
|
326
|
-
{ type: "separator" },
|
|
327
329
|
{ label: "Close Session", accelerator: "CmdOrCtrl+W", click: () => sendMenu("close-session") },
|
|
328
|
-
{ label: "Close Window", accelerator: isMac ? "Cmd+Shift+W" : "Alt+F4", role: "close" },
|
|
329
330
|
{ type: "separator" },
|
|
330
331
|
isMac ? { role: "close" } : { role: "quit", label: "Exit" },
|
|
331
332
|
],
|
|
@@ -353,8 +354,6 @@ function buildMenu() {
|
|
|
353
354
|
{ role: "resetZoom" },
|
|
354
355
|
{ role: "zoomIn" },
|
|
355
356
|
{ role: "zoomOut" },
|
|
356
|
-
{ type: "separator" },
|
|
357
|
-
{ label: "Toggle Theme", accelerator: "CmdOrCtrl+Shift+D", click: () => sendMenu("toggle-theme") },
|
|
358
357
|
{ role: "toggleDevTools" },
|
|
359
358
|
],
|
|
360
359
|
},
|
|
@@ -363,28 +362,13 @@ function buildMenu() {
|
|
|
363
362
|
submenu: [
|
|
364
363
|
{ label: "Next Session", accelerator: "Ctrl+Tab", click: () => sendMenu("next-session") },
|
|
365
364
|
{ label: "Previous Session", accelerator: "Ctrl+Shift+Tab", click: () => sendMenu("prev-session") },
|
|
366
|
-
{ type: "separator" },
|
|
367
|
-
{ label: "Export Session Log…", accelerator: "CmdOrCtrl+Shift+E", click: () => sendMenu("export-session") },
|
|
368
|
-
{ type: "separator" },
|
|
369
365
|
// IPC audit #6: the "List Sessions" entry dispatched 'list-sessions',
|
|
370
366
|
// a command with NO SPA-side handler — deleted (dead menu surface).
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
{ role: "minimize" },
|
|
377
|
-
{ role: "zoom" },
|
|
378
|
-
...(isMac ? [
|
|
379
|
-
{ type: "separator" },
|
|
380
|
-
{ role: "front" },
|
|
381
|
-
] : []),
|
|
382
|
-
],
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
role: "help",
|
|
386
|
-
submenu: [
|
|
387
|
-
{ label: "Summrise Agent Status", click: () => { focusMain(); sendMenu("show-status"); } },
|
|
367
|
+
// The "Export Session Log…" entry (^⇧E) was deleted for the same kind of
|
|
368
|
+
// reason read the other way: the per-tab export mark IS the action
|
|
369
|
+
// (TabBar's .tab-export, one mark per session, so it exports what you
|
|
370
|
+
// point at rather than whatever happens to be active). The accelerator
|
|
371
|
+
// is still handled by the SPA's own keydown map in a plain browser.
|
|
388
372
|
],
|
|
389
373
|
},
|
|
390
374
|
];
|
|
@@ -671,7 +655,9 @@ function embeddedRecover() {
|
|
|
671
655
|
return { ok: false };
|
|
672
656
|
// Already decided by loadTarget() when embeddedUrl was set (the ONE load
|
|
673
657
|
// door) — recovery re-loads that decision, it does not re-open the policy.
|
|
674
|
-
|
|
658
|
+
// The fallback is about:blank, NOT a third-party home page: recovering a
|
|
659
|
+
// crashed view must not make a request to a search engine to do it.
|
|
660
|
+
const url = embeddedUrl || "about:blank";
|
|
675
661
|
view.webContents.loadURL(url).catch(() => { });
|
|
676
662
|
// Re-show if the SPA slot is live (bounds were placed before the crash).
|
|
677
663
|
if (embeddedVisible && win && embeddedBounds) {
|
|
@@ -906,12 +892,31 @@ const httpServer = http.createServer((req, res) => {
|
|
|
906
892
|
}
|
|
907
893
|
});
|
|
908
894
|
if (gotTheLock) {
|
|
909
|
-
//
|
|
910
|
-
//
|
|
895
|
+
// TASKBAR GROUPING AND THE TASKBAR GROUP'S ICON — HALF ONE OF TWO, AND THIS IS THE HALF
|
|
896
|
+
// THAT IS A PRECONDITION RATHER THAN A HINT.
|
|
897
|
+
// Microsoft's rule for System.AppUserModel.RelaunchIconResource is a three-step lookup:
|
|
898
|
+
// the property ON THE WINDOW; else "the system attempts to find a shortcut with the same
|
|
899
|
+
// AppUserModelID, and pins that shortcut to the taskbar to represent the window"; else
|
|
900
|
+
// "the backing executable of the process that owns it is used". Which step is reachable
|
|
901
|
+
// at all depends on THIS call — the same page: the shortcut lookup happens only "If an
|
|
902
|
+
// explicit AppUserModelID is set on the window", and without one the property "is ignored
|
|
903
|
+
// and the window is grouped and pinned as if it were part of its owning process".
|
|
904
|
+
// This shell is launched UNPACKAGED (`electron.exe .` out of the staged runtime), so
|
|
905
|
+
// step three is stock electron.exe — the Electron logo. Removing this call (d4edbb64)
|
|
906
|
+
// could not fix that: it deleted the caller's half of an association whose OTHER half is
|
|
907
|
+
// the shortcut, which left step three as the only reachable step in BOTH states. The
|
|
908
|
+
// shortcut half is written in agent/summrise-agent-npm/src/summrise.ts (see DESKTOP_AUMID
|
|
909
|
+
// there) as System.AppUserModel.ID — the same string — plus RelaunchIconResource pointing
|
|
910
|
+
// at the sunrise .ico. KEEP THE TWO IN STEP; the literal below is a copy because this file
|
|
911
|
+
// is emitted to plain JS in two packages and cannot import the CLI's module.
|
|
912
|
+
// (The same shortcut association is also the documented precondition for the hide-to-tray
|
|
913
|
+
// toast below: Electron requires a Start Menu shortcut carrying the ID and a
|
|
914
|
+
// ToastActivatorCLSID, and this one carries neither — unchanged by this call.)
|
|
915
|
+
const AUMID = "online.saisi.summrise.agent";
|
|
911
916
|
try {
|
|
912
917
|
if (process.platform === "win32")
|
|
913
|
-
electron_1.app.setAppUserModelId(
|
|
914
|
-
iconReport["appUserModelId"] = process.platform === "win32" ?
|
|
918
|
+
electron_1.app.setAppUserModelId(AUMID);
|
|
919
|
+
iconReport["appUserModelId"] = process.platform === "win32" ? AUMID : "(non-windows)";
|
|
915
920
|
}
|
|
916
921
|
catch {
|
|
917
922
|
iconReport["appUserModelId"] = "(set-failed)";
|
|
@@ -934,6 +939,16 @@ if (gotTheLock) {
|
|
|
934
939
|
electron_1.Menu.setApplicationMenu(buildMenu());
|
|
935
940
|
win = new electron_1.BrowserWindow({
|
|
936
941
|
width: 1200, height: 800, title: "Summrise",
|
|
942
|
+
// NO WHITE FLASH (window-creation honesty, round-275). The window used to be
|
|
943
|
+
// created VISIBLE, before loadDesktop() runs its ~800 ms /api/status probe and
|
|
944
|
+
// before the first loadURL — so every launch painted a blank white frame and then
|
|
945
|
+
// whatever the agent turned out to be. show:false keeps it off screen until the
|
|
946
|
+
// first document has painted (ready-to-show, below); backgroundColor paints that
|
|
947
|
+
// first frame in the shell's own ink instead of Chromium's white, which is the one
|
|
948
|
+
// of the two documents that had NOT already fixed this (WAIT_HTML hardcodes
|
|
949
|
+
// background:#111, and the SPA's chrome is dark).
|
|
950
|
+
show: false,
|
|
951
|
+
backgroundColor: "#111",
|
|
937
952
|
// Taskbar + title-bar icon: without this Windows shows the stock
|
|
938
953
|
// electron.exe icon (the running binary is stock Electron). PNG —
|
|
939
954
|
// see windowIcon() on why not .ico here.
|
|
@@ -955,6 +970,11 @@ if (gotTheLock) {
|
|
|
955
970
|
backgroundThrottling: false,
|
|
956
971
|
},
|
|
957
972
|
});
|
|
973
|
+
// The other half of show:false — the first paint shows the window. It fires on the
|
|
974
|
+
// first document to paint (the SPA when the agent answers, the wait page when it does
|
|
975
|
+
// not), and the handler is registered in the same tick as the constructor, before any
|
|
976
|
+
// loadURL can happen, so there is no window in which the event is missed.
|
|
977
|
+
win.once("ready-to-show", () => { win?.show(); });
|
|
958
978
|
// review #6 (MED) TOP RISK: the main window carries the summriseDesktop/
|
|
959
979
|
// summriseBrowser preload bridge (setAutoLaunch → schtasks /create …
|
|
960
980
|
// -File <ps1>, browser-session:open). Without a navigation veto a
|
|
@@ -1048,25 +1068,15 @@ if (gotTheLock) {
|
|
|
1048
1068
|
let retryMs = 2000;
|
|
1049
1069
|
const nextRetry = () => { retryMs = Math.min(retryMs * 2, 30000); return retryMs; };
|
|
1050
1070
|
const resetRetry = () => { retryMs = 2000; };
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
clearTimeout(t);
|
|
1061
|
-
if (r.ok) {
|
|
1062
|
-
const j = await r.json();
|
|
1063
|
-
const v = j.release || j.version;
|
|
1064
|
-
if (v)
|
|
1065
|
-
win?.setTitle(`Summrise — v${v}`);
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
catch { /* agent down — keep default title */ }
|
|
1069
|
-
};
|
|
1071
|
+
// NO setVersionTitle() HERE, DELETED. It fetched /api/status with the device token and
|
|
1072
|
+
// called win.setTitle("Summrise — v1.2.x"), and that title NEVER REACHED THE SCREEN: the
|
|
1073
|
+
// SPA owns document.title (`useAttentionTitle` assigns it on every attention change,
|
|
1074
|
+
// panel-react/src/hooks/useAttention.ts:96 — plain "Summrise Agent" in the desktop window)
|
|
1075
|
+
// and no page-title-updated handler on THIS window stops it, so the renderer's title
|
|
1076
|
+
// replaced the native one on the first render. The version it wanted to show is already on
|
|
1077
|
+
// screen, in the panel's own status bar (DesktopShell's .desktop-status) and in the tray
|
|
1078
|
+
// tooltip, both of which read the same /api/status. A native title that only the process
|
|
1079
|
+
// could read was also the LAST credentialed fetch on this path — the tray keeps its own.
|
|
1070
1080
|
// stage-n: SELF-HEAL watchdog. The wait-page button needed a human, but
|
|
1071
1081
|
// when the AGENT is down the cloudflared tunnel (agent-supervised) dies
|
|
1072
1082
|
// too — a remote operator then has NO channel left to click anything
|
|
@@ -1085,7 +1095,6 @@ if (gotTheLock) {
|
|
|
1085
1095
|
if (await agentReady()) {
|
|
1086
1096
|
agentMissCount = 0;
|
|
1087
1097
|
resetRetry();
|
|
1088
|
-
setVersionTitle();
|
|
1089
1098
|
win?.loadURL(`${(0, url_policy_1.agentBase)()}/desktop/`).catch(() => { });
|
|
1090
1099
|
}
|
|
1091
1100
|
else {
|
|
@@ -1125,11 +1134,13 @@ if (gotTheLock) {
|
|
|
1125
1134
|
// without it, playwright grabbed the MAIN window (now tripwired) or the
|
|
1126
1135
|
// user had to open the Browser page first. The view is hidden until the
|
|
1127
1136
|
// SPA's Browser page reports bounds (embeddedViewPlace shows it).
|
|
1137
|
+
// NO EAGER NAVIGATION: this used to loadURL("https://www.bing.com"), i.e. a
|
|
1138
|
+
// request to a third party on EVERY launch before anyone asked for a page.
|
|
1139
|
+
// The view is still created — about:blank is a first-class CDP target (the
|
|
1140
|
+
// reason it exists here is that a target be available, not that a page be
|
|
1141
|
+
// loaded), and the user/AI navigates it next (loadTarget is the one door).
|
|
1128
1142
|
try {
|
|
1129
|
-
|
|
1130
|
-
if (v && !v.webContents.isDestroyed()) {
|
|
1131
|
-
v.webContents.loadURL("https://www.bing.com").catch(() => { });
|
|
1132
|
-
}
|
|
1143
|
+
embeddedViewEnsure();
|
|
1133
1144
|
}
|
|
1134
1145
|
catch { /* non-fatal */ }
|
|
1135
1146
|
console.log(`[summrise] CDP endpoint: http://127.0.0.1:${CDP_PORT} (playwright connectOverCDP)`);
|
|
@@ -1195,6 +1206,9 @@ if (gotTheLock) {
|
|
|
1195
1206
|
// status strip shows the same pair).
|
|
1196
1207
|
let trayAgentCpu = null;
|
|
1197
1208
|
let trayAgentMem = null;
|
|
1209
|
+
/** WHEN /api/status last ANSWERED (null = it never has since launch) — the tray's
|
|
1210
|
+
* tooltip reports this instead of asserting a state it cannot date. */
|
|
1211
|
+
let trayLastAnsweredAt = null;
|
|
1198
1212
|
/** "3m 24s" / "1h 5m" / "2d 3h" — compact uptime for the tray. */
|
|
1199
1213
|
const fmtUptime = (secs) => {
|
|
1200
1214
|
if (secs < 60)
|
|
@@ -1205,10 +1219,16 @@ if (gotTheLock) {
|
|
|
1205
1219
|
return `${Math.floor(secs / 3600)}h ${Math.floor((secs % 3600) / 60)}m`;
|
|
1206
1220
|
return `${Math.floor(secs / 86400)}d ${Math.floor((secs % 86400) / 3600)}h`;
|
|
1207
1221
|
};
|
|
1222
|
+
/** "14:02:31" local — the WHEN of an observation, for the one surface that cannot
|
|
1223
|
+
* re-derive it (a tray tooltip is a string set once per poll, not a render). */
|
|
1224
|
+
const fmtClock = (at) => new Date(at).toTimeString().slice(0, 8);
|
|
1208
1225
|
const refreshTray = async () => {
|
|
1209
1226
|
// HTTP liveness, not the TCP probe (see agentReady above): a wedged-
|
|
1210
1227
|
// but-listening agent must show as STOPPED, not "running".
|
|
1211
1228
|
const running = await agentResponds(1000);
|
|
1229
|
+
const observedAt = Date.now();
|
|
1230
|
+
if (running)
|
|
1231
|
+
trayLastAnsweredAt = observedAt;
|
|
1212
1232
|
let version = trayAgentVersion;
|
|
1213
1233
|
let uptime = trayAgentUptime;
|
|
1214
1234
|
let sessions = trayAgentSessions;
|
|
@@ -1248,9 +1268,19 @@ if (gotTheLock) {
|
|
|
1248
1268
|
const vitals = running && mem !== null
|
|
1249
1269
|
? `, CPU ${cpu === null ? "?" : Math.round(cpu)}% · MEM ${Math.round(mem)}%`
|
|
1250
1270
|
: "";
|
|
1271
|
+
// THE TOOLTIP NAMES THE OBSERVATION, NOT A VERDICT (round-275). It used to print
|
|
1272
|
+
// "Agent stopped" as present tense from ONE un-timestamped probe, recomputed every
|
|
1273
|
+
// 30 s — half a minute stale while reading as now, the shape the panel's liveness
|
|
1274
|
+
// strings were already fixed for (`checked 12s ago`, `reachability NOT VERIFIED`).
|
|
1275
|
+
// The WHEN is a CLOCK rather than "answered 12s ago", deliberately: a tooltip is a
|
|
1276
|
+
// string written once per poll and Electron gives it no way to recompute, so a
|
|
1277
|
+
// relative age would freeze at the value it had when written and read as "now" for
|
|
1278
|
+
// the next 30 s — the very failure this line removes. A timestamp does not decay.
|
|
1251
1279
|
const health = running
|
|
1252
|
-
? `
|
|
1253
|
-
:
|
|
1280
|
+
? `answered ${fmtClock(observedAt)} · v${version || "?"}${uptime ? `, up ${uptime}` : ""}${sessions ? `, ${sessions} session${sessions === 1 ? "" : "s"}` : ""}${vitals}`
|
|
1281
|
+
: trayLastAnsweredAt === null
|
|
1282
|
+
? "reachability NOT VERIFIED · no reply since launch"
|
|
1283
|
+
: `not answering · last reply ${fmtClock(trayLastAnsweredAt)}`;
|
|
1254
1284
|
// stage-n: the agent died WHILE the window was showing the SPA (the
|
|
1255
1285
|
// wait page only renders when the boot probe fails). Swap the window
|
|
1256
1286
|
// to the wait page — its "Start Agent" button is now actually visible
|
|
@@ -1266,16 +1296,15 @@ if (gotTheLock) {
|
|
|
1266
1296
|
if (running && agentWatchActive)
|
|
1267
1297
|
agentWatchActive = false;
|
|
1268
1298
|
tray.setToolTip(`Summrise — ${health}`);
|
|
1299
|
+
// THE FOUR "New …" ROWS ARE GONE (round-275): they duplicated the desktop
|
|
1300
|
+
// header's `+ New` menu item for item (pty/ssh/serial/browser), one click
|
|
1301
|
+
// cheaper than the tray path, which had to restore and focus the window
|
|
1302
|
+
// first anyway. "Open" is the tray's job; creating sessions is the app's.
|
|
1269
1303
|
tray.setContextMenu(electron_1.Menu.buildFromTemplate([
|
|
1270
1304
|
{ label: "Open", click: () => { focusMain(); } },
|
|
1271
1305
|
{ type: "separator" },
|
|
1272
1306
|
{ label: health, enabled: false },
|
|
1273
1307
|
{ type: "separator" },
|
|
1274
|
-
{ label: "New Terminal", click: () => { focusMain(); sendMenu("new-pty"); } },
|
|
1275
|
-
{ label: "New SSH…", click: () => { focusMain(); sendMenu("new-ssh"); } },
|
|
1276
|
-
{ label: "New Serial…", click: () => { focusMain(); sendMenu("new-serial"); } },
|
|
1277
|
-
{ label: "New Browser…", click: () => { focusMain(); sendMenu("new-browser"); } },
|
|
1278
|
-
{ type: "separator" },
|
|
1279
1308
|
{ label: "Quit", click: () => { electron_1.app.isQuitting = true; electron_1.app.quit(); } },
|
|
1280
1309
|
]));
|
|
1281
1310
|
};
|