strom-research 1.24.0 → 1.25.0-beta.1

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/CHANGELOG.cs.md CHANGED
@@ -5,6 +5,15 @@
5
5
  Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou na
6
6
  [GitHub Releases](https://github.com/ACiDekCZ/strom-research/releases) (anglicky).
7
7
 
8
+ ## 1.25.0
9
+
10
+ *Připravuje se: bety do 1.25.0-beta.1. Oddíl bude úplný s vydáním.*
11
+
12
+ - **Data pro widget Strom i na Windows.** Výzkum teď zapisuje to, co z něj ukazuje widget Strom (na čem se pracuje,
13
+ nálezy za posledních 24 hodin, co čeká), i na Windows, do `%USERPROFILE%\.config\strom\widget`, se značkou verze
14
+ strom, která je zapsala. Widget, který zrovna čte svůj soubor, most nikdy nezdrží. `strom uninstall` odebere i soubory
15
+ widgetu, také na Macu; výzkum a nastavení zůstávají.
16
+
8
17
  ## 1.24.0
9
18
 
10
19
  *31. 10. 2026 · patří k aplikaci Strom 3.19.0.*
package/CHANGELOG.de.md CHANGED
@@ -5,6 +5,16 @@
5
5
  Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Installationszeilen stehen bei
6
6
  [GitHub Releases](https://github.com/ACiDekCZ/strom-research/releases) (auf Englisch).
7
7
 
8
+ ## 1.25.0
9
+
10
+ *In Vorbereitung: die Betas bis 1.25.0-beta.1. Der Abschnitt ist mit dem Release vollständig.*
11
+
12
+ - **Daten für das Strom-Widget auch unter Windows.** Eine Forschung schreibt jetzt, was das Strom-Widget von ihr zeigt
13
+ (woran gearbeitet wird, die Funde der letzten 24 Stunden, was wartet), auch unter Windows, nach
14
+ `%USERPROFILE%\.config\strom\widget`, mit einer Marke des strom, der es geschrieben hat. Ein Widget, das gerade seine
15
+ Datei liest, hält die Brücke nie auf. `strom uninstall` entfernt auch die Dateien des Widgets, auch auf dem Mac; die
16
+ Forschung und die Einstellungen bleiben.
17
+
8
18
  ## 1.24.0
9
19
 
10
20
  *31.10.2026 · gehört zur Strom-App 3.19.0.*
package/CHANGELOG.md CHANGED
@@ -5,6 +5,15 @@ English · [Čeština](CHANGELOG.cs.md) · [Deutsch](CHANGELOG.de.md)
5
5
  The changes of each version in short. The full notes and the install lines are on
6
6
  [GitHub Releases](https://github.com/ACiDekCZ/strom-research/releases).
7
7
 
8
+ ## 1.25.0
9
+
10
+ *In preparation: the betas up to 1.25.0-beta.1. The section is complete with the release.*
11
+
12
+ - **Data for the Strom widget on Windows too.** A research now writes what the Strom widget shows of it (what it works
13
+ on, the finds of the last 24 hours, what waits) also on Windows, into `%USERPROFILE%\.config\strom\widget`, with a
14
+ mark of the strom that wrote it. A widget reading its file never holds up the bridge. `strom uninstall` takes the
15
+ widget's files too, on a Mac as well; the research and the settings stay.
16
+
8
17
  ## 1.24.0
9
18
 
10
19
  *2026-10-31 · goes with the Strom app 3.19.0.*
@@ -881,6 +881,7 @@
881
881
  "ui.uninstall.links": "odkazy z aplikace Strom",
882
882
  "ui.uninstall.git": "vlastní git stromu",
883
883
  "ui.uninstall.path": "záznam instalátoru v PATH",
884
+ "ui.uninstall.widget": "soubory widgetu Strom",
884
885
  "ui.uninstall.program": "program",
885
886
  "ui.uninstall.list": "strom z počítače odebere:",
886
887
  "ui.uninstall.keeps": "Výzkumy zůstávají ({home}), stejně jako nastavení s klíči, které je pečetí ({config}) – smazat je ručně, jen pokud už nikdy nebudou potřeba.",
@@ -881,6 +881,7 @@
881
881
  "ui.uninstall.links": "die Links aus der Strom-App",
882
882
  "ui.uninstall.git": "strom's eigenes git",
883
883
  "ui.uninstall.path": "der PATH-Eintrag des Installationsprogramms",
884
+ "ui.uninstall.widget": "die Dateien des Strom-Widgets",
884
885
  "ui.uninstall.program": "das Programm",
885
886
  "ui.uninstall.list": "strom entfernt von diesem Computer:",
886
887
  "ui.uninstall.keeps": "Die Forschungen bleiben ({home}), ebenso die Einstellungen mit den Schlüsseln, die sie versiegeln ({config}) – sie nur dann selbst löschen, wenn sie nie wieder gebraucht werden.",
package/dist/cli/main.js CHANGED
@@ -38,6 +38,7 @@ import { expandFromLine, refreshLinks } from "../core/links.js";
38
38
  import { clockLine, FINISH_LINE, finishLimitLine, finishOf, timeLine } from "../core/clock.js";
39
39
  import { currentSession } from "../core/session.js";
40
40
  import { backgroundOf } from "../core/fetching.js";
41
+ import { markWidget } from "../core/widget.js";
41
42
  import { callMistake, checkArgs, GroupOnly, parseOptions, resolveCommand, firstWord, severalRecords, splitPassthrough, usageOf } from "./execute.js";
42
43
  import { placeholders, UI, ui } from "./ui.js";
43
44
  import { catalog, localized, placeholdersOf, sayCommandAs } from "../core/phrases.js";
@@ -410,6 +411,8 @@ async function mainOf(argv, io, env, cwd) {
410
411
  ctx.settings.config.lastChannel = channel;
411
412
  ctx.settings.save();
412
413
  refreshGlobal(env);
414
+ // …and the widget on the desktop (macOS, Windows) learns this strom writes its files: index.json, even with no tree
415
+ markWidget(env);
413
416
  // …and the links from the Strom app lead to this strom again (where strom made them on this person's yes —
414
417
  // never another installation's: one with its own settings asks)
415
418
  try {
package/dist/cli/ui.js CHANGED
@@ -718,6 +718,7 @@ export const UI = {
718
718
  "ui.uninstall.links": "the links from the Strom app",
719
719
  "ui.uninstall.git": "strom's own git",
720
720
  "ui.uninstall.path": "the installer's PATH entry",
721
+ "ui.uninstall.widget": "the Strom widget's files",
721
722
  "ui.uninstall.program": "the program",
722
723
  "ui.uninstall.list": "strom takes this off the computer:",
723
724
  "ui.uninstall.keeps": "The research stays ({home}), and so do the settings with the keys that seal it ({config}) — delete them by hand only if they are never wanted again.",
@@ -68,6 +68,7 @@ import { ensureHooksDir } from "../core/hooks.js";
68
68
  import { claudeInChrome, CLAUDE_IN_CHROME_URL, downloadsDir } from "../core/browser.js";
69
69
  import { browserConnectors, fenceKeepsNetOff, listConnectors } from "../core/connector.js";
70
70
  import { appReadLimits } from "../core/live-read.js";
71
+ import { markWidget } from "../core/widget.js";
71
72
  export const SHARED_DIRS = ["media", "catalog", "tools", "cache", "inbox"];
72
73
  export function ensureShared(dir) {
73
74
  for (const d of SHARED_DIRS)
@@ -94,6 +95,8 @@ register({
94
95
  const s = ctx.settings;
95
96
  if (ctx.interactive && !isAgent(ctx.env)) {
96
97
  const r = await setupWizard(ctx, { research: Boolean(opts.research) });
98
+ // the widget on the desktop (macOS, Windows) learns this strom writes its files
99
+ markWidget(ctx.env);
97
100
  // run in an archive (the Strom app's link to the research's settings too): research offered beside it
98
101
  let root;
99
102
  try {
@@ -184,6 +187,7 @@ register({
184
187
  cfg.trees = trees;
185
188
  s.save();
186
189
  fs.mkdirSync(home, { recursive: true });
190
+ markWidget(ctx.env);
187
191
  const sharedDir = s.shared().value;
188
192
  const treesDir = s.trees().value;
189
193
  fs.mkdirSync(treesDir, { recursive: true });
@@ -497,7 +497,7 @@ register({
497
497
  },
498
498
  }, {
499
499
  path: ["uninstall"],
500
- summary: "Take strom off this computer: what the agents were taught, the shortcut, its own git, its PATH lines and the program — the research and the settings stay",
500
+ summary: "Take strom off this computer: what the agents were taught, the shortcut, its own git, its PATH lines, the widget's files and the program — the research and the settings stay",
501
501
  group: "setup",
502
502
  description: "The user's decision: a person at their terminal is asked there (--yes: no question); asked by an agent, strom asks\n" +
503
503
  "in a window of the system. Installed through npm, the program itself goes with npm uninstall -g strom-research.",
package/dist/core/json.js CHANGED
@@ -42,7 +42,7 @@ export function stringifyCanonical(value) {
42
42
  export const RENAME_PATIENCE = 1500;
43
43
  const RENAME_FIRST_WAIT = 25;
44
44
  /** What another program holding the target (an antivirus, the search indexer, a backup) makes a rename fail with. */
45
- const HELD = new Set(["EPERM", "EBUSY", "EACCES"]);
45
+ export const HELD = new Set(["EPERM", "EBUSY", "EACCES"]);
46
46
  /**
47
47
  * fn, again for a while on Windows when a file it uses is held: an antivirus, the indexer or a sync tool that has the
48
48
  * file open for a moment makes a rename, a read or a removal fail with EPERM, EBUSY or EACCES. The waits grow (25, 50,
@@ -88,14 +88,20 @@ export function readFileRetrying(file, platform = process.platform) {
88
88
  export function removeRetrying(file, platform = process.platform) {
89
89
  retryingHeld(file, platform, () => fs.rmSync(file, { force: true }));
90
90
  }
91
- /** Write via a temp file in the same directory and rename: never a half file. */
92
- export function writeFileAtomic(file, content) {
91
+ /**
92
+ * Write via a temp file in the same directory and rename: never a half file. `patient: false`: the rename tried once,
93
+ * never waited for a file another program holds (the widget's files: the bridge's tick is never held up by a reader).
94
+ */
95
+ export function writeFileAtomic(file, content, opts = {}) {
93
96
  const dir = path.dirname(file);
94
97
  fs.mkdirSync(dir, { recursive: true });
95
98
  const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`);
96
99
  fs.writeFileSync(tmp, content, "utf8");
97
100
  try {
98
- renameRetrying(tmp, file);
101
+ if (opts.patient === false)
102
+ fs.renameSync(tmp, file);
103
+ else
104
+ renameRetrying(tmp, file);
99
105
  }
100
106
  catch (err) {
101
107
  fs.rmSync(tmp, { force: true });
package/dist/core/live.js CHANGED
@@ -3727,8 +3727,8 @@ export function serveLive(root, env) {
3727
3727
  return atWorkNow;
3728
3728
  };
3729
3729
  let workers = safe("the start", () => JSON.stringify(atWork(seen))) ?? "";
3730
- // the research on the desktop of a Mac (the Strom Widget): the tree's overview written while the bridge runs
3731
- // (core/widget.ts; none off macOS) — the first a moment after the start, never in its way
3730
+ // the research on the desktop (the Strom Widget of a Mac, the widget of Windows 11): the tree's overview written
3731
+ // while the bridge runs (core/widget.ts; none but on macOS and Windows) — the first a moment after the start, never in its way
3732
3732
  const widget = widgetWriter(root, env);
3733
3733
  if (widget)
3734
3734
  setTimeout(() => safe("the widget", () => widget.tick(true)), 1000).unref?.();
package/dist/core/tree.js CHANGED
@@ -29,7 +29,7 @@ export const TREE_FILE = "strom.json";
29
29
  export const SEAL_FOREIGN = "seal.foreign";
30
30
  // between releases the candidate being made (a bridge run from the sources says what it is: the Strom app goes by it);
31
31
  // at a release the release's own (package.json)
32
- export const VERSION = "1.24.0";
32
+ export const VERSION = "1.25.0-beta.1";
33
33
  const GITIGNORE = `# Strom Research — local state and large files are not versioned
34
34
  .strom/
35
35
  /media/
@@ -1,8 +1,9 @@
1
1
  // What strom put on this computer outside the research, and taking it away
2
2
  // again (strom uninstall): what the agents were taught, the shortcut, strom's
3
- // own git, the PATH lines of the installer and the program itself. The
4
- // research stays, and so do the settings with the keys that seal it — they
5
- // are the user's; strom says where they are.
3
+ // own git, the PATH lines of the installer, the widget's files (what it
4
+ // showed of the research) and the program itself. The research stays, and so
5
+ // do the settings with the keys that seal it — they are the user's; strom
6
+ // says where they are.
6
7
  import fs from "node:fs";
7
8
  import path from "node:path";
8
9
  import { spawn, spawnSync } from "node:child_process";
@@ -13,6 +14,7 @@ import { globalTargets, isInstalled, uninstallGlobal } from "../agents/global.js
13
14
  import { PROFILES } from "../agents/profiles.js";
14
15
  import { holdsPath, installation, pathForms, PROGRAM_ENTRIES } from "./self.js";
15
16
  import { linkFiles, linkOwner, unregisterLinks } from "./links.js";
17
+ import { widgetDir } from "./widget.js";
16
18
  /** The mark the installer (install/install.sh) puts on the PATH line it adds. */
17
19
  const MARK = "# strom research";
18
20
  const sys = (cmd, args) => {
@@ -328,6 +330,11 @@ export function uninstallPlan(env, names, opts = {}) {
328
330
  // none
329
331
  }
330
332
  }
333
+ // the widget's files (macOS, Windows): only what strom wrote there, the folder when nothing else is left in it — the
334
+ // widget would show the research as not at work for ever
335
+ const widget = widgetDir(env, platform);
336
+ if (widget && widgetFiles(widget).length && !holdsKept(widget, keep, platform))
337
+ remove.push({ kind: "widget", path: widget, remove: () => removeWidgetFiles(widget) });
331
338
  const program = inst.kind === "installed" ? inst.root : undefined;
332
339
  // install.json's launchers: those of a launcher's name outside the research only
333
340
  const chosen = launchersToTake(inst.launchers ?? [], keep, platform);
@@ -346,3 +353,26 @@ export function uninstallPlan(env, names, opts = {}) {
346
353
  const skipped = chosen.skipped.filter((l) => !pathForms(l, platform).some((f) => taken.has(f)));
347
354
  return { remove, npm: inst.kind === "npm", ...(program ? { program, launchers: chosen.take, skipped } : {}) };
348
355
  }
356
+ /** What strom writes into the widget's folder: index.json, each tree's <ID>.json and a file being written beside one. */
357
+ const WIDGET_FILE = /^(?:index|[0-9A-Za-z-]{1,64})\.json$|^\..+\.json\.\d+\.[0-9a-f]+\.tmp$/u;
358
+ export function widgetFiles(dir) {
359
+ try {
360
+ return fs.readdirSync(dir).filter((n) => WIDGET_FILE.test(n) && fs.statSync(path.join(dir, n)).isFile());
361
+ }
362
+ catch {
363
+ return [];
364
+ }
365
+ }
366
+ /** The widget's files taken; the folder too when nothing else is in it (never what strom did not write there). */
367
+ export function removeWidgetFiles(dir) {
368
+ for (const n of widgetFiles(dir))
369
+ fs.rmSync(path.join(dir, n), { force: true });
370
+ try {
371
+ if (!fs.readdirSync(dir).length)
372
+ fs.rmdirSync(dir);
373
+ }
374
+ catch {
375
+ // gone already, or another program's file in it: it stays
376
+ }
377
+ return !widgetFiles(dir).length;
378
+ }
@@ -4,20 +4,22 @@
4
4
  // phone-finds.ts) and cleaned as for it (forPhone): never a path, a record's ID, a spend or a model. Beside each text
5
5
  // that names a person, the same without the names (the widget's "hide names").
6
6
  //
7
- // Written by the tree's bridge (strom live) while it runs, on macOS alone: ~/Library/Application Support/Strom/widget/
8
- // <tree id>.json and index.json (the trees there), each whole (a file beside it renamed over it). Looked at every
9
- // WIDGET_CHECK_MS, written when what it says changed, else every WIDGET_HEARTBEAT_MS; the bridge ending as it should
10
- // writes `running: false`, its research gone takes the file away. An archive has none. The widget only reads: nothing
11
- // of it comes back. An isolated installation (STROM_ISOLATED: a test's, a second strom) writes into its own settings
12
- // folder — a research of a test never shows in the person's widget.
7
+ // Written by the tree's bridge (strom live) while it runs, on macOS (~/Library/Application Support/Strom/widget/) and
8
+ // Windows (%USERPROFILE%\.config\strom\widget\, the Windows 11 widget of strom-win): <tree id>.json and index.json
9
+ // (the trees there), each whole (a file beside it renamed over it). Looked at every WIDGET_CHECK_MS, written when what
10
+ // it says changed, else every WIDGET_HEARTBEAT_MS; the bridge ending as it should writes `running: false`, its research
11
+ // gone takes the file away. An archive has none. The widget only reads: nothing of it comes back. An isolated
12
+ // installation (STROM_ISOLATED: a test's, a second strom) writes into its own settings folder — a research of a test
13
+ // never shows in the person's widget. index.json is there from this strom's first run and its setup on (`strom`: the
14
+ // version that wrote it), so the widget tells a strom too old to write any from one whose bridge has not run yet.
13
15
  import fs from "node:fs";
14
16
  import path from "node:path";
15
17
  import { configDir, isolated, userHome } from "./paths.js";
16
18
  import { Settings } from "./config.js";
17
- import { Tree } from "./tree.js";
19
+ import { Tree, VERSION } from "./tree.js";
18
20
  import { actsAsArchive } from "./state.js";
19
21
  import { foldText } from "./text.js";
20
- import { writeFileAtomic } from "./json.js";
22
+ import { writeFileAtomic, HELD } from "./json.js";
21
23
  import { appUrlSetting } from "./stromapp.js";
22
24
  import { forPhone, treeOverview } from "./phone-watch.js";
23
25
  import { treeFinds } from "./phone-finds.js";
@@ -37,17 +39,23 @@ export const WIDGET_PENDING = 3;
37
39
  /** What stands for a name taken out of a text. */
38
40
  const HIDDEN = "…";
39
41
  /**
40
- * Where the widget's files are written: none but on macOS; STROM_WIDGET_DIR when set; an isolated installation into its
41
- * own settings folder; else ~/Library/Application Support/Strom/widget of the HOME strom runs with.
42
+ * Where the widget's files are written: none but on macOS and Windows; STROM_WIDGET_DIR when set; an isolated
43
+ * installation into its own settings folder; else on macOS ~/Library/Application Support/Strom/widget of the HOME strom
44
+ * runs with, on Windows %USERPROFILE%\.config\strom\widget — the profile's own folder (HOME may be Git Bash's), also
45
+ * with settings elsewhere (STROM_CONFIG_DIR), as the Mac's ~/Library. Never under AppData: an app packaged as MSIX
46
+ * (Claude's desktop app, whose agent starts the bridge) gets every new file there as a private copy of its own, which
47
+ * the widget would never see (found 2026-10-30 with the seal keys; core/paths.ts configDir).
42
48
  */
43
49
  export function widgetDir(env, platform = process.platform) {
44
- if (platform !== "darwin")
50
+ if (platform !== "darwin" && platform !== "win32")
45
51
  return undefined;
46
52
  const said = env.STROM_WIDGET_DIR?.trim();
47
53
  if (said)
48
54
  return path.resolve(said);
49
55
  if (isolated(env))
50
56
  return path.join(configDir(env, platform), "widget");
57
+ if (platform === "win32")
58
+ return path.join(env.USERPROFILE || userHome(env), ".config", "strom", "widget");
51
59
  return path.join(userHome(env), "Library", "Application Support", "Strom", "widget");
52
60
  }
53
61
  /** A moment as the widget reads it: ISO 8601 to the second, in UTC (Swift's .iso8601 reads no fractions). */
@@ -318,15 +326,37 @@ export function widgetTree(root, env, opts) {
318
326
  const treeFile = (dir, key) => path.join(dir, `${key}.json`);
319
327
  /** A tree's key as a file's name: the tree's ID (a UUID) — never a path. */
320
328
  const KEY = /^[0-9A-Za-z-]{1,64}$/u;
321
- /** index.json as the files of the trees there say it, written whole when it says something else. */
329
+ /**
330
+ * A file of the widget written whole (a file beside it renamed over it) — at once or not now: the widget reading it this
331
+ * moment holds it on Windows (a rename over an open file fails with EPERM or EBUSY there), and the bridge never waits
332
+ * for it (writeFileAtomic's patience would hold the bridge's tick for 1.5 s). False: not written, nothing left beside
333
+ * it; the next look writes it.
334
+ */
335
+ function writeNow(file, content) {
336
+ try {
337
+ writeFileAtomic(file, content, { patient: false });
338
+ return true;
339
+ }
340
+ catch (err) {
341
+ if (HELD.has(err.code ?? ""))
342
+ return false;
343
+ throw err;
344
+ }
345
+ }
346
+ /**
347
+ * index.json as the files of the trees there say it, written whole when it says something else (or another strom wrote
348
+ * it); the folder made when there is none (the mark of this strom: strom's first run, its setup). False: held by the
349
+ * widget this moment, not written.
350
+ */
322
351
  export function writeWidgetIndex(dir, now = Date.now()) {
323
352
  const trees = [];
324
353
  let names = [];
325
354
  try {
326
355
  names = fs.readdirSync(dir).filter((n) => n.endsWith(".json") && n !== "index.json" && !n.startsWith("."));
327
356
  }
328
- catch {
329
- return;
357
+ catch (err) {
358
+ if (err.code !== "ENOENT")
359
+ return true;
330
360
  }
331
361
  for (const n of names.sort()) {
332
362
  try {
@@ -342,34 +372,64 @@ export function writeWidgetIndex(dir, now = Date.now()) {
342
372
  const file = path.join(dir, "index.json");
343
373
  try {
344
374
  const was = JSON.parse(fs.readFileSync(file, "utf8"));
345
- if (JSON.stringify(was.trees) === JSON.stringify(trees) && was.schemaVersion === WIDGET_SCHEMA)
346
- return;
375
+ if (JSON.stringify(was.trees) === JSON.stringify(trees) && was.schemaVersion === WIDGET_SCHEMA && was.strom === VERSION)
376
+ return true;
347
377
  }
348
378
  catch {
349
379
  // none yet
350
380
  }
351
- writeFileAtomic(file, `${JSON.stringify({ schemaVersion: WIDGET_SCHEMA, generatedAt: isoSecond(now), trees }, null, 2)}\n`);
381
+ return writeNow(file, `${JSON.stringify({ schemaVersion: WIDGET_SCHEMA, generatedAt: isoSecond(now), strom: VERSION, trees }, null, 2)}\n`);
382
+ }
383
+ /**
384
+ * The mark that this strom writes the widget's files (on macOS and Windows): index.json, with no tree in it when no
385
+ * bridge ran yet — written at strom's first run of a version and at its setup. A widget that finds none tells a strom too
386
+ * old to write any (strom update) from one whose bridge has not run yet. Never in the way: any failure said nowhere.
387
+ */
388
+ export function markWidget(env, platform = process.platform) {
389
+ const dir = widgetDir(env, platform);
390
+ if (!dir)
391
+ return;
392
+ try {
393
+ writeWidgetIndex(dir);
394
+ }
395
+ catch {
396
+ // written by the next bridge
397
+ }
352
398
  }
353
- /** A tree's file written whole (a file beside it renamed over it), then index.json. */
399
+ /** A tree's file written whole (a file beside it renamed over it), then index.json. False: held by the widget, not now. */
354
400
  export function writeWidgetTree(dir, data) {
355
401
  if (!KEY.test(data.treeKey))
356
- return;
357
- writeFileAtomic(treeFile(dir, data.treeKey), `${JSON.stringify(data, null, 2)}\n`);
358
- writeWidgetIndex(dir, Date.parse(data.generatedAt) || Date.now());
402
+ return true;
403
+ if (!writeNow(treeFile(dir, data.treeKey), `${JSON.stringify(data, null, 2)}\n`))
404
+ return false;
405
+ return writeWidgetIndex(dir, Date.parse(data.generatedAt) || Date.now());
359
406
  }
360
- /** A tree's file taken away (its research gone, an archive now), and index.json without it. */
361
- export function dropWidgetTree(dir, key) {
407
+ /**
408
+ * A tree's file taken away (its research gone, an archive now), and index.json without it. One the widget holds without
409
+ * letting it go (Windows) stays: it says at least `running: false` (as `last` was, or as it says), not a research at work
410
+ * for the quarter of an hour the widget takes to call it old.
411
+ */
412
+ export function dropWidgetTree(dir, key, last) {
362
413
  if (!KEY.test(key))
363
414
  return;
415
+ const file = treeFile(dir, key);
364
416
  try {
365
- fs.rmSync(treeFile(dir, key), { force: true });
417
+ fs.rmSync(file, { force: true });
366
418
  }
367
419
  catch {
420
+ try {
421
+ const was = last ?? JSON.parse(fs.readFileSync(file, "utf8"));
422
+ const at = isoSecond(Date.now());
423
+ writeWidgetTree(dir, { ...was, running: false, generatedAt: at, lastActiveAt: at });
424
+ }
425
+ catch {
426
+ // nothing more to do: the widget calls it old by itself
427
+ }
368
428
  return;
369
429
  }
370
430
  writeWidgetIndex(dir);
371
431
  }
372
- /** The writer of a tree's bridge; none off macOS. */
432
+ /** The writer of a tree's bridge; none but on macOS and Windows. */
373
433
  export function widgetWriter(root, env, platform = process.platform, clock = Date.now) {
374
434
  const dir = widgetDir(env, platform);
375
435
  if (!dir)
@@ -381,40 +441,54 @@ export function widgetWriter(root, env, platform = process.platform, clock = Dat
381
441
  let said = "";
382
442
  let last;
383
443
  let key;
444
+ // what the widget held at the last look (Windows: renamed over an open file): written at the next tick, not the next look
445
+ let held;
384
446
  // what moves with the clock alone says nothing new
385
447
  const content = (t) => JSON.stringify({ ...t, generatedAt: "", lastActiveAt: "", findings: { ...t.findings, windowStart: "" } });
448
+ const write = (t, c, now) => {
449
+ if (!writeWidgetTree(dir, t)) {
450
+ held = { t, c };
451
+ return;
452
+ }
453
+ held = undefined;
454
+ said = c;
455
+ written = now;
456
+ };
386
457
  return {
387
458
  tick(force = false) {
388
459
  const now = clock();
389
- if (!force && now - checked < checkMs)
460
+ if (!force && now - checked < checkMs) {
461
+ if (held)
462
+ write({ ...held.t, generatedAt: isoSecond(now), lastActiveAt: isoSecond(now) }, held.c, now);
390
463
  return;
464
+ }
391
465
  checked = now;
392
466
  const t = widgetTree(root, env, { running: true, now });
393
467
  if (!t) {
394
468
  // an archive now: nothing of it on the desktop
395
469
  if (key)
396
- dropWidgetTree(dir, key);
470
+ dropWidgetTree(dir, key, last);
397
471
  key = undefined;
398
472
  last = undefined;
473
+ held = undefined;
399
474
  said = "";
400
475
  return;
401
476
  }
402
477
  key = t.treeKey;
403
478
  last = t;
404
479
  const c = content(t);
405
- if (c === said && now - written < beatMs)
480
+ if (c === said && now - written < beatMs && !held)
406
481
  return;
407
- writeWidgetTree(dir, t);
408
- said = c;
409
- written = now;
482
+ write(t, c, now);
410
483
  },
411
484
  end(gone) {
412
485
  if (!key)
413
486
  return;
414
487
  // what the file says now is no longer what was written: the next look writes it again
415
488
  said = "";
489
+ held = undefined;
416
490
  if (gone)
417
- return dropWidgetTree(dir, key);
491
+ return dropWidgetTree(dir, key, last && { ...last, running: false });
418
492
  if (!last)
419
493
  return;
420
494
  const at = isoSecond(clock());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strom-research",
3
- "version": "1.24.0",
3
+ "version": "1.25.0-beta.1",
4
4
  "description": "Genealogical research toolkit for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -57,5 +57,8 @@
57
57
  "devDependencies": {
58
58
  "@types/node": "^22.20.4",
59
59
  "typescript": "^7.0.2"
60
+ },
61
+ "publishConfig": {
62
+ "tag": "beta"
60
63
  }
61
64
  }