strom-research 1.20.0 → 1.21.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,17 @@
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.21.0
9
+
10
+ *Připravuje se: bety do 1.21.0-beta.1. Oddíl bude úplný s vydáním.*
11
+
12
+ - **„Čeká“ říká, co aplikace Strom neukazuje.** Řádek v menu a `strom plan` řekne, kolik z čekajícího je jen tady
13
+ v terminálu a kolik čeká na odpověď; zbytek je číslo, které ukazuje aplikace Strom, takže se obě shodují.
14
+ - **„Co čeká“ dává vlastní věci terminálu na začátek.** Co ukazuje jen terminál (rozpor vlastní úpravy, odeslání
15
+ z aplikace, otázka ke čtení skenů), je pod „Jen tady v terminálu“, zbytek pod „I v aplikaci Strom“.
16
+ - **`strom input assign`, `hide` a `unskip` řeknou, co se změnilo**, v jazyce výzkumu: každý soubor, každý
17
+ uvolněný nebo nový úkol, lidi jménem a jak změnu vrátit. Jejich JSON zůstává stejný.
18
+
8
19
  ## 1.20.0
9
20
 
10
21
  *25. 10. 2026 · patří k aplikaci Strom 3.16.0.*
package/CHANGELOG.de.md CHANGED
@@ -5,6 +5,19 @@
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.21.0
9
+
10
+ *In Vorbereitung: die Betas bis 1.21.0-beta.1. Der Abschnitt ist mit dem Release vollständig.*
11
+
12
+ - **„Wartet“ sagt, was die Strom-App nicht zeigt.** Die Zeile im Menü und `strom plan` sagt, wie viel des Wartenden
13
+ nur hier im Terminal ist und wie viel auf Antwort wartet; der Rest ist die Zahl, die die Strom-App zeigt, sodass
14
+ beide übereinstimmen.
15
+ - **„Was wartet“ stellt das Eigene des Terminals voran.** Was nur das Terminal zeigt (ein Widerspruch einer eigenen
16
+ Änderung, eine Sendung der App, eine Frage zum Lesen von Scans), steht unter „Nur hier im Terminal“, der Rest unter
17
+ „Auch in der Strom-App“.
18
+ - **`strom input assign`, `hide` und `unskip` sagen, was sich geändert hat**, in der Sprache der Forschung: jede
19
+ Datei, jede freigegebene oder neue Aufgabe, Personen mit Namen und wie es rückgängig geht. Ihr JSON bleibt gleich.
20
+
8
21
  ## 1.20.0
9
22
 
10
23
  *25.10.2026 · gehört zur Strom-App 3.16.0.*
package/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ 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.21.0
9
+
10
+ *In preparation: the betas up to 1.21.0-beta.1. The section is complete with the release.*
11
+
12
+ - **"Waiting" says what the Strom app does not show.** The line in the menu and `strom plan` says how many of the
13
+ waiting things are only here in the terminal and how many wait for a reply; the rest is the number the Strom app
14
+ shows, so the two agree.
15
+ - **"What waits" puts the terminal's own first.** What only the terminal shows (a conflict of an own edit, a send of
16
+ the app, a question about the reading of scans) is listed under "Only here in the terminal", the rest under
17
+ "Also in the Strom app".
18
+ - **`strom input assign`, `hide` and `unskip` say what changed** in the research's language: each file, each task
19
+ let go of or made, people by name, and how to take it back. Their JSON stays the same.
20
+
8
21
  ## 1.20.0
9
22
 
10
23
  *2026-10-25 · goes with the Strom app 3.16.0.*
@@ -90,6 +90,9 @@
90
90
  "ui.menu.title": "Strom výzkum {version}",
91
91
  "ui.menu.tree": "Rodokmen: {name} · osob: {persons}",
92
92
  "ui.menu.waiting": "Čeká: {count}",
93
+ "ui.waiting.split.reply": "{n, plural, one {# čeká} few {# čekají} other {# čeká}} na odpověď",
94
+ "ui.waiting.split.here": "{n} jen tady v terminálu",
95
+ "ui.waiting.split": "{n} (z toho {parts})",
93
96
  "ui.menu.working": "Agent právě pracuje samostatně.",
94
97
  "ui.menu.chat": "Pokračovat ve výzkumu s agentem",
95
98
  "ui.menu.start": "Začít výzkum s agentem",
@@ -448,6 +451,8 @@
448
451
  "ui.wait.how1": "snímky patří do schránky (inbox), pro každou knihu vlastní složka, každý pojmenovaný číslem snímku, jak ho počítá prohlížeč",
449
452
  "ui.wait.how2": "(9.jpg; přiblížená část: 9a.jpg) – další běh je zaregistruje a úkol se sám vrátí do fronty",
450
453
  "ui.wait.todo.n": "k vyřízení {n}",
454
+ "ui.wait.app.head": "I v aplikaci Strom · {n}",
455
+ "ui.wait.only.head": "Jen tady v terminálu (v aplikaci Strom nejsou) · {n}",
451
456
  "ui.wait.sent.n": "čeká na odpověď {n}",
452
457
  "ui.wait.sent.title": "Čeká na odpověď",
453
458
  "ui.wait.filter": "filtr: {section}",
@@ -1633,7 +1638,7 @@
1633
1638
  "ui.plan.images": " (nejdřív potřebuje snímky)",
1634
1639
  "ui.plan.more": "… a další úkoly: {n}",
1635
1640
  "ui.plan.stories": "Potom vyprávění: {n} ({names})",
1636
- "ui.plan.waiting": "Čeká: {n} (v menu: Co čeká)",
1641
+ "ui.plan.waiting": "Čeká: {count} · v menu: Co čeká",
1637
1642
  "ui.plan.none": "Fronta je prázdná – nové úkoly agent navrhne po dalším sezení, nebo mu jde v rozhovoru říct, co hledat.",
1638
1643
  "ui.plan.steer": "Pořadí se mění v rozhovoru s agentem: stačí mu říct, co má přednost.",
1639
1644
  "ui.plan.session": "sezení z {day}",
@@ -1869,6 +1874,24 @@
1869
1874
  "ui.intake.scans.archive": "Ve složce {folder} {n, plural, one {je # obrázek. Uložit ho do Archivu Stromu jako rodinný dokument?} few {jsou # obrázky. Uložit je do Archivu Stromu jako rodinné dokumenty?} other {je # obrázků. Uložit je do Archivu Stromu jako rodinné dokumenty?}}",
1870
1875
  "ui.intake.book.archive": "{folder}: neuloženo.",
1871
1876
  "ui.intake.known": "Tyhle podklady už ve výzkumu jsou – nic nepřibylo.",
1877
+ "ui.input.assign.add": "{id} – nově patří k: {who}",
1878
+ "ui.input.assign.remove": "{id} – už nepatří k: {who}",
1879
+ "ui.input.batch.add": "dávka {name} – nově patří k: {who}",
1880
+ "ui.input.batch.remove": "dávka {name} – už nepatří k: {who}",
1881
+ "ui.input.task.of": "úkol {task} – nově se týká i: {who}",
1882
+ "ui.input.assign.kept": "{id}: {who} zůstává – patří k celé dávce (odebrat z dávky: strom input assign --batch {batch} --remove {person})",
1883
+ "ui.input.assign.none": "Nic ke změně.",
1884
+ "ui.input.hidden": "{id} skryto",
1885
+ "ui.input.why.unrelated": "nic z rodiny",
1886
+ "ui.input.why.duplicate": "totéž jako jiný soubor",
1887
+ "ui.input.task.without": "úkol {task} – bez {files} (skryto)",
1888
+ "ui.input.task.dropped": "úkol {task} zrušen – jeho soubory jsou skryté ({files})",
1889
+ "ui.input.hide.back": "zobrazit znovu: strom input unskip {ids}",
1890
+ "ui.input.hide.none": "Už skryto – nic se nezměnilo.",
1891
+ "ui.input.shown.new": "{id} znovu zobrazeno (čeká na roztřídění)",
1892
+ "ui.input.shown.processed": "{id} znovu zobrazeno (zpracováno)",
1893
+ "ui.input.task.gather": "úkol {task}: {what}",
1894
+ "ui.input.unskip.none": "Nic skrytého – nic se nezměnilo.",
1872
1895
  "ui.intake.done": "✓ Přidáno do výzkumu: {n}. Agent to projde při další práci.",
1873
1896
  "ui.intake.done.archive": "✓ Uloženo do Archivu Stromu: {n}.",
1874
1897
  "ui.intake.done.waiting": "✓ Uloženo do výzkumu: {n}. Agent to projde, až bude na počítači.",
@@ -90,6 +90,9 @@
90
90
  "ui.menu.title": "Strom Ahnenforschung {version}",
91
91
  "ui.menu.tree": "Stammbaum: {name} · Personen: {persons}",
92
92
  "ui.menu.waiting": "Wartet: {count}",
93
+ "ui.waiting.split.reply": "{n, plural, one {# wartet} other {# warten}} auf Antwort",
94
+ "ui.waiting.split.here": "{n} nur hier im Terminal",
95
+ "ui.waiting.split": "{n} (davon {parts})",
93
96
  "ui.menu.working": "Der Agent arbeitet gerade selbstständig.",
94
97
  "ui.menu.chat": "Die Forschung mit dem Agenten fortsetzen",
95
98
  "ui.menu.start": "Die Forschung mit dem Agenten beginnen",
@@ -448,6 +451,8 @@
448
451
  "ui.wait.how1": "Scans kommen in den Eingang (inbox), ein Ordner pro Buch, jeder benannt nach der Bildnummer, wie der Viewer sie zählt",
449
452
  "ui.wait.how2": "(9.jpg; ein vergrößerter Ausschnitt: 9a.jpg) – der nächste Lauf registriert sie und die Aufgabe kehrt von selbst zurück",
450
453
  "ui.wait.todo.n": "zu erledigen {n}",
454
+ "ui.wait.app.head": "Auch in der Strom-App · {n}",
455
+ "ui.wait.only.head": "Nur hier im Terminal (nicht in der Strom-App) · {n}",
451
456
  "ui.wait.sent.n": "wartet auf Antwort {n}",
452
457
  "ui.wait.sent.title": "Wartet auf Antwort",
453
458
  "ui.wait.filter": "Filter: {section}",
@@ -1633,7 +1638,7 @@
1633
1638
  "ui.plan.images": " (braucht zuerst die Scans)",
1634
1639
  "ui.plan.more": "… und {n, plural, one {# weitere Aufgabe} other {# weitere Aufgaben}}",
1635
1640
  "ui.plan.stories": "Danach die Geschichten: {n} ({names})",
1636
- "ui.plan.waiting": "Wartet: {n} (im Menü: Was wartet)",
1641
+ "ui.plan.waiting": "Wartet: {count} · im Menü: Was wartet",
1637
1642
  "ui.plan.none": "Die Warteschlange ist leer – neue Aufgaben schlägt der Agent nach der nächsten Sitzung vor, oder ihm im Gespräch sagen, was er suchen soll.",
1638
1643
  "ui.plan.steer": "Die Reihenfolge ändert sich im Gespräch mit dem Agenten: ihm sagen, was Vorrang hat.",
1639
1644
  "ui.plan.session": "Sitzung vom {day}",
@@ -1869,6 +1874,24 @@
1869
1874
  "ui.intake.scans.archive": "Im Ordner {folder} {n, plural, one {ist # Bild. Als Familiendokument im Strom-Archiv speichern?} other {sind # Bilder. Als Familiendokumente im Strom-Archiv speichern?}}",
1870
1875
  "ui.intake.book.archive": "{folder}: nicht gespeichert.",
1871
1876
  "ui.intake.known": "Diese Unterlagen sind schon in der Forschung – nichts hinzugefügt.",
1877
+ "ui.input.assign.add": "{id} – gehört jetzt auch zu: {who}",
1878
+ "ui.input.assign.remove": "{id} – gehört nicht mehr zu: {who}",
1879
+ "ui.input.batch.add": "Stapel {name} – gehört jetzt auch zu: {who}",
1880
+ "ui.input.batch.remove": "Stapel {name} – gehört nicht mehr zu: {who}",
1881
+ "ui.input.task.of": "Aufgabe {task} – betrifft jetzt auch: {who}",
1882
+ "ui.input.assign.kept": "{id}: {who} bleibt – gehört zum ganzen Stapel (vom Stapel entfernen: strom input assign --batch {batch} --remove {person})",
1883
+ "ui.input.assign.none": "Nichts zu ändern.",
1884
+ "ui.input.hidden": "{id} ausgeblendet",
1885
+ "ui.input.why.unrelated": "nichts von der Familie",
1886
+ "ui.input.why.duplicate": "dasselbe wie eine andere Datei",
1887
+ "ui.input.task.without": "Aufgabe {task} – ohne {files} (ausgeblendet)",
1888
+ "ui.input.task.dropped": "Aufgabe {task} aufgegeben – ihre Dateien sind ausgeblendet ({files})",
1889
+ "ui.input.hide.back": "wieder zeigen: strom input unskip {ids}",
1890
+ "ui.input.hide.none": "Bereits ausgeblendet – nichts geändert.",
1891
+ "ui.input.shown.new": "{id} wieder sichtbar (wartet auf Sortierung)",
1892
+ "ui.input.shown.processed": "{id} wieder sichtbar (verarbeitet)",
1893
+ "ui.input.task.gather": "Aufgabe {task}: {what}",
1894
+ "ui.input.unskip.none": "Nichts ausgeblendet – nichts geändert.",
1872
1895
  "ui.intake.done": "✓ Zur Forschung hinzugefügt: {n}. Der Agent sieht es bei der nächsten Arbeit durch.",
1873
1896
  "ui.intake.done.archive": "✓ Im Strom-Archiv gespeichert: {n}.",
1874
1897
  "ui.intake.done.waiting": "✓ In der Forschung aufbewahrt: {n}. Der Agent sieht es durch, sobald er auf diesem Computer ist.",
@@ -3,7 +3,9 @@
3
3
  // an office, one message to the family. What is to do apart from what waits for a reply from outside (sent).
4
4
  //
5
5
  // the overview the places of what is to do, a page of 15 at a time (next, previous, a range, all), filtered by
6
- // section, "?" for what waits for a reply; a number opens a place — a place of one thing that thing
6
+ // section, "?" for what waits for a reply; a number opens a place — a place of one thing that thing;
7
+ // what the Strom app does not show (core/waiting.ts terminalOnly) first, under a heading of its own,
8
+ // the app's after it under theirs
7
9
  // a place its things, then for all of them at once: the request texts joined, sent, a conversation
8
10
  // a thing what it is and waits for, then only what makes sense for its kind: the archive or the site, the
9
11
  // folder for images saved by hand, the request's text, sent (or back among what is to do), an answer
@@ -26,7 +28,7 @@ import { claimOf, isEditConflict, sideText } from "../core/conflicts.js";
26
28
  import { openForUser } from "../core/open.js";
27
29
  import { isArchive } from "../core/mode.js";
28
30
  import { listConnectors } from "../core/connector.js";
29
- import { requestFile, requestTexts, waitingFor } from "../core/waiting.js";
31
+ import { inApp, requestFile, requestTexts, waitingFor } from "../core/waiting.js";
30
32
  import { SENDABLE } from "../core/waitkind.js";
31
33
  /** A person for the person reading: "Jan Novák (*1905) [P0001]". */
32
34
  const label = (p) => `${displayName(p)}${lifespan(p) ? ` (${lifespan(p)})` : ""} [${p.id}]`;
@@ -87,16 +89,25 @@ function short(tree, lang, w) {
87
89
  }
88
90
  const sentAtOf = (w) => String(w.item.sentAt ?? w.item.at);
89
91
  /** The places of a list, in its order (the items of one place stand together). */
90
- function placesOf(items) {
92
+ function placesOf(items, only = false) {
91
93
  const by = new Map();
92
94
  for (const w of items) {
93
95
  let p = by.get(w.item.group.key);
94
96
  if (!p)
95
- by.set(w.item.group.key, (p = { key: w.item.group.key, name: w.item.group.name, items: [] }));
97
+ by.set(w.item.group.key, (p = { key: w.item.group.key, name: w.item.group.name, items: [], only }));
96
98
  p.items.push(w);
97
99
  }
98
100
  return [...by.values()];
99
101
  }
102
+ /**
103
+ * The places of the overview: when something is only here in the terminal, its places first, apart from those the
104
+ * Strom app shows too (a place with both — an archive's milder question beside its tasks — in each part with its own);
105
+ * else the places as they come.
106
+ */
107
+ function overviewPlaces(items) {
108
+ const here = items.filter((w) => !inApp(w));
109
+ return here.length ? [...placesOf(here, true), ...placesOf(items.filter(inApp))] : placesOf(items);
110
+ }
100
111
  /** A place's line on the overview: the thing itself when it is one, else what is there by kind. */
101
112
  function placeDetail(t, lang, tree, p, phase) {
102
113
  const one = p.items.length === 1 ? truncate(short(tree, lang, p.items[0]), 60) : undefined;
@@ -144,7 +155,9 @@ export async function waitingForYou(ctx, run, lang, root) {
144
155
  counts.set(w.item.section, (counts.get(w.item.section) ?? 0) + 1);
145
156
  if (filter && !counts.get(filter))
146
157
  filter = undefined;
147
- const places = placesOf(filter ? here.filter((w) => w.item.section === filter) : here);
158
+ const places = overviewPlaces(filter ? here.filter((w) => w.item.section === filter) : here);
159
+ // the part only here in the terminal and the part the Strom app shows too, each with its heading
160
+ const split = places.some((p) => p.only);
148
161
  const pages = Math.max(1, Math.ceil(places.length / PAGE));
149
162
  page = Math.min(page, pages - 1);
150
163
  const [from, to] = view === "all" ? [0, places.length] : view ? [view.from, Math.min(view.to, places.length)] : [page * PAGE, Math.min(places.length, (page + 1) * PAGE)];
@@ -166,6 +179,10 @@ export async function waitingForYou(ctx, run, lang, root) {
166
179
  const width = Math.max(0, ...names.map((n) => [...n].length));
167
180
  for (const [k, p] of shown.entries()) {
168
181
  const name = names[k];
182
+ if (split && (k === 0 || shown[k - 1].only !== p.only)) {
183
+ const n = places.filter((x) => x.only === p.only).reduce((a, x) => a + x.items.length, 0);
184
+ screen.push(` ${t(p.only ? "ui.wait.only.head" : "ui.wait.app.head", { n })}`);
185
+ }
169
186
  screen.push(`${String(from + k + 1).padStart(2)} ${name}${" ".repeat(width - [...name].length)} ${placeDetail(t, lang, tree, p, phase)}`);
170
187
  }
171
188
  // what can be typed here, only what does something now
@@ -247,7 +264,7 @@ export async function waitingForYou(ctx, run, lang, root) {
247
264
  if (/^\d+$/u.test(a) && Number(a) >= 1 && Number(a) <= places.length) {
248
265
  const p = places[Number(a) - 1];
249
266
  // a place of one: that thing at once
250
- await guarded(ctx, lang, async () => (p.items.length === 1 ? thing(ctx, run, lang, root, p.items[0]) : place(ctx, run, lang, root, p.key, phase, filter)));
267
+ await guarded(ctx, lang, async () => (p.items.length === 1 ? thing(ctx, run, lang, root, p.items[0]) : place(ctx, run, lang, root, p.key, phase, filter, split ? p.only : undefined)));
251
268
  break;
252
269
  }
253
270
  out(controlLine);
@@ -255,14 +272,14 @@ export async function waitingForYou(ctx, run, lang, root) {
255
272
  }
256
273
  }
257
274
  /** One place: its things, then for all of them at once — the request texts joined, sent, a conversation. */
258
- async function place(ctx, run, lang, root, key, phase, filter) {
275
+ async function place(ctx, run, lang, root, key, phase, filter, only) {
259
276
  const t = translator(lang);
260
277
  const out = (line = "") => ctx.io.stdout(line + "\n");
261
278
  for (;;) {
262
279
  if (outOfAnswers(ctx))
263
280
  return;
264
281
  const tree = Tree.open(root, ctx.env);
265
- const items = waitingHere(ctx, tree).filter((w) => w.item.group.key === key && w.item.phase === phase && (!filter || w.item.section === filter));
282
+ const items = waitingHere(ctx, tree).filter((w) => w.item.group.key === key && w.item.phase === phase && (!filter || w.item.section === filter) && (only === undefined || only === !inApp(w)));
266
283
  if (!items.length)
267
284
  return;
268
285
  const name = items[0].item.group.name;
package/dist/cli/menu.js CHANGED
@@ -43,6 +43,7 @@ import { pathOf, startAgain } from "../core/userpath.js";
43
43
  import { offerAgent } from "./fixes.js";
44
44
  import { addToResearch } from "./menu-research.js";
45
45
  import { waitingForYou, waitingHere } from "./menu-waiting.js";
46
+ import { waitingLine } from "../core/waiting.js";
46
47
  import { permissionWaitingLines } from "../core/archiveasks.js";
47
48
  import { settingsMenu } from "./menu-settings.js";
48
49
  import { mainPerson } from "../core/kin.js";
@@ -177,7 +178,8 @@ export async function runMenu(ctx, run) {
177
178
  }
178
179
  // what waits for the person: the same list What waits shows (each thing once — a task that waits for an archive's
179
180
  // decision is one of its question's)
180
- const waiting = waitingHere(ctx, tree).length;
181
+ const waits = waitingHere(ctx, tree);
182
+ const waiting = waits.length;
181
183
  const started = tree.list("research").length > 0;
182
184
  out(t("ui.menu.tree", { name: tree.config.name, persons: tree.countLive("person") }));
183
185
  // No agent yet (none installed, or not the one chosen): the research waits for it — kept as an archive meanwhile,
@@ -201,8 +203,9 @@ export async function runMenu(ctx, run) {
201
203
  out();
202
204
  }
203
205
  }
206
+ // what of it the Strom app does not show (only here in the terminal) said, so its number and the app's agree
204
207
  if (waiting)
205
- out(t("ui.menu.waiting", { count: waiting }));
208
+ out(waitingLine(lang, waits));
206
209
  // an archive's images wait for the person's decision: one line, only until it is answered
207
210
  // (since when, and the tasks that stand on it, once it waits long or holds up much)
208
211
  for (const line of permissionWaitingLines(tree, lang))
package/dist/cli/ui.js CHANGED
@@ -63,6 +63,9 @@ export const UI = {
63
63
  "ui.menu.title": "Strom research {version}",
64
64
  "ui.menu.tree": "Family tree: {name} · people: {persons}",
65
65
  "ui.menu.waiting": "Waiting: {count}",
66
+ "ui.waiting.split": "{n} (of these {parts})",
67
+ "ui.waiting.split.here": "{n} only here in the terminal",
68
+ "ui.waiting.split.reply": "{n} waiting for a reply",
66
69
  "ui.menu.working": "The agent is working on its own right now.",
67
70
  "ui.menu.chat": "Continue the research with the agent",
68
71
  "ui.menu.start": "Start the research with the agent",
@@ -425,6 +428,8 @@ export const UI = {
425
428
  "ui.wait.how1": "scans go into the inbox, a folder per book, each named by its image number as the viewer counts it",
426
429
  "ui.wait.how2": "(9.jpg; a zoomed-in part of it: 9a.jpg) — the next run registers them and the task comes back by itself",
427
430
  "ui.wait.todo.n": "to do {n}",
431
+ "ui.wait.only.head": "Only here in the terminal (not in the Strom app) · {n}",
432
+ "ui.wait.app.head": "Also in the Strom app · {n}",
428
433
  "ui.wait.sent.n": "waiting for a reply {n}",
429
434
  "ui.wait.sent.title": "Waiting for a reply",
430
435
  "ui.wait.filter": "filter: {section}",
@@ -1622,7 +1627,7 @@ export const UI = {
1622
1627
  "ui.plan.images": " (needs the scans first)",
1623
1628
  "ui.plan.more": "… and {n, plural, one {# more task} other {# more tasks}}",
1624
1629
  "ui.plan.stories": "Then the stories: {n} ({names})",
1625
- "ui.plan.waiting": "Waiting: {n} (in the menu: What waits)",
1630
+ "ui.plan.waiting": "Waiting: {count} · in the menu: What waits",
1626
1631
  "ui.plan.none": "Nothing in the queue — the agent proposes new tasks after its next session, or tell it in a conversation what to look for.",
1627
1632
  "ui.plan.steer": "To change the order, tell the agent in a conversation what comes first.",
1628
1633
  "ui.plan.session": "the session of {day}",
@@ -1858,6 +1863,24 @@ export const UI = {
1858
1863
  "ui.intake.scans.archive": "The folder {folder} holds {n, plural, one {# image. Save it to the Strom archive as a family document?} other {# images. Save them to the Strom archive as family documents?}}",
1859
1864
  "ui.intake.book.archive": "{folder}: not saved.",
1860
1865
  "ui.intake.known": "This material is in the research already — nothing added.",
1866
+ "ui.input.assign.add": "{id} – now also of: {who}",
1867
+ "ui.input.assign.remove": "{id} – no longer of: {who}",
1868
+ "ui.input.batch.add": "batch {name} – now also of: {who}",
1869
+ "ui.input.batch.remove": "batch {name} – no longer of: {who}",
1870
+ "ui.input.task.of": "task {task} – now also about: {who}",
1871
+ "ui.input.assign.kept": "{id}: {who} stays – the whole batch is of them (off the batch: strom input assign --batch {batch} --remove {person})",
1872
+ "ui.input.assign.none": "Nothing to change.",
1873
+ "ui.input.hidden": "{id} hidden",
1874
+ "ui.input.why.unrelated": "nothing of the family",
1875
+ "ui.input.why.duplicate": "the same as another file",
1876
+ "ui.input.task.without": "task {task} – without {files} (hidden)",
1877
+ "ui.input.task.dropped": "task {task} dropped – its files are hidden ({files})",
1878
+ "ui.input.hide.back": "to show again: strom input unskip {ids}",
1879
+ "ui.input.hide.none": "Hidden already – nothing changed.",
1880
+ "ui.input.shown.new": "{id} shown again (waits to be sorted)",
1881
+ "ui.input.shown.processed": "{id} shown again (processed)",
1882
+ "ui.input.task.gather": "task {task}: {what}",
1883
+ "ui.input.unskip.none": "Nothing hidden – nothing changed.",
1861
1884
  "ui.intake.done": "✓ Added to the research: {n}. The agent goes through it in its next work.",
1862
1885
  "ui.intake.done.archive": "✓ Saved to the Strom archive: {n}.",
1863
1886
  "ui.intake.done.waiting": "✓ Kept in the research: {n}. The agent goes through it once it is on this computer.",
@@ -17,7 +17,9 @@ import { lacksImages } from "../core/queue.js";
17
17
  import { forStory } from "../core/stories.js";
18
18
  import { isArchive } from "../core/mode.js";
19
19
  import { actsAsArchive, stateOf } from "../core/state.js";
20
- import { rankedQueue, waitingForUser } from "./tasks.js";
20
+ import { rankedQueue } from "./tasks.js";
21
+ import { waitingHere } from "../cli/menu-waiting.js";
22
+ import { waitingLine } from "../core/waiting.js";
21
23
  import { readTuneLog } from "../core/tune.js";
22
24
  import { calibrationLabel } from "../core/viewsizes.js";
23
25
  /** A change strom made by itself in the reading of scans, as a person reads it in strom recent. */
@@ -205,7 +207,9 @@ register({
205
207
  // the stories, with the searches beyond the registers before them
206
208
  const work = queue.filter((t) => !forStory(t));
207
209
  const stories = queue.filter(forStory);
208
- const waiting = waitingForUser(tree).length;
210
+ // what waits for the person: the menu's What waits, what of it is only here in the terminal said (core/waiting.ts)
211
+ const waits = waitingHere(ctx, tree);
212
+ const waiting = waits.length;
209
213
  const shown = work.slice(0, count);
210
214
  let n = 0;
211
215
  const rows = shown.map((t) => [
@@ -225,7 +229,7 @@ register({
225
229
  out.push(ui(lang, "ui.plan.stories", { n: stories.length, names: storyNames.join(", ") }));
226
230
  }
227
231
  if (waiting)
228
- out.push(ui(lang, "ui.plan.waiting", { n: waiting }));
232
+ out.push(waitingLine(lang, waits, "ui.plan.waiting"));
229
233
  if (queue.length)
230
234
  out.push("", ui(lang, "ui.plan.steer"));
231
235
  const item = (t) => ({ id: t.id, what: humanTask(tree, t.what, lang), level: t.level, state: t.state, needsImages: lacksImages(tree, t) });
@@ -14,7 +14,7 @@ import { collectFiles, fileSha256, inputPath, MAX_IN_TREE, mimeOf, storeShared }
14
14
  import { create, normId, requireRecord, update } from "../core/records.js";
15
15
  import { phrase } from "../core/phrases.js";
16
16
  import { addMarriedNames, importedTree, importGedcom, importStromJson, isStromJson, marriedNamesPlan, undoMarriedNames } from "../core/import.js";
17
- import { gedcomName, likelyDuplicates } from "../core/people.js";
17
+ import { displayName, gedcomName, likelyDuplicates } from "../core/people.js";
18
18
  import { decodeText, safeFolderName } from "../core/text.js";
19
19
  import { amendInput, checkOriginalMeta } from "../core/originals.js";
20
20
  import { batchStatus, closeBatch, listBatches, readBatch } from "../core/batches.js";
@@ -61,6 +61,69 @@ function idsGiven(ctx, args, opts) {
61
61
  return ids;
62
62
  }
63
63
  /** The research the inputs are for: the one named, else the only active one. */
64
+ /** A person for the person reading, as the material names them: "Jan Novák [P0002]" (an ID the research lacks as given). */
65
+ function whoOf(tree, ids) {
66
+ return ids
67
+ .map((id) => {
68
+ const p = tree.get(id);
69
+ return p ? `${displayName(p)} [${p.id}]` : id;
70
+ })
71
+ .join(", ");
72
+ }
73
+ /** The tasks' files before a write: what a task let go of is said by its files (strom input hide). */
74
+ function filesOfTasks(tree) {
75
+ return new Map(tree.list("task").map((t) => [t.id, t.where.filter((w) => /^I\d/u.test(w))]));
76
+ }
77
+ /**
78
+ * What strom input assign|hide|unskip changed, in the research language (the person reads it — the menu, the Strom
79
+ * app's bridge, an agent too): each file, each task, each batch, from what this write logged and how the records are
80
+ * now. The history keeps its own words.
81
+ */
82
+ function materialLines(tree, before, batch) {
83
+ const lang = tree.lang;
84
+ const out = [];
85
+ for (const o of tree.written) {
86
+ const [id, ...rest] = o.targets;
87
+ if (o.op === "input.assign" && id) {
88
+ const i = tree.get(id);
89
+ const now = new Set([...(i?.persons ?? []), ...(i?.sorted?.persons ?? [])]);
90
+ const add = rest.filter((p) => now.has(p));
91
+ const remove = rest.filter((p) => !now.has(p));
92
+ if (add.length)
93
+ out.push(ui(lang, "ui.input.assign.add", { id, who: whoOf(tree, add) }));
94
+ if (remove.length)
95
+ out.push(ui(lang, "ui.input.assign.remove", { id, who: whoOf(tree, remove) }));
96
+ }
97
+ else if (o.op === "batch.assign" && batch) {
98
+ const b = readBatch(tree.root, batch);
99
+ const now = new Set(b?.persons ?? []);
100
+ const name = b?.name ?? batch;
101
+ const add = o.targets.filter((p) => now.has(p));
102
+ const remove = o.targets.filter((p) => !now.has(p));
103
+ if (add.length)
104
+ out.push(ui(lang, "ui.input.batch.add", { name, who: whoOf(tree, add) }));
105
+ if (remove.length)
106
+ out.push(ui(lang, "ui.input.batch.remove", { name, who: whoOf(tree, remove) }));
107
+ }
108
+ else if (o.op === "task.edit" && id && rest.length)
109
+ out.push(ui(lang, "ui.input.task.of", { task: id, who: whoOf(tree, rest) }));
110
+ else if (o.op === "task.edit" && id) {
111
+ const t = tree.get(id);
112
+ const files = (before.get(id) ?? []).filter((f) => !t?.where.includes(f));
113
+ if (files.length)
114
+ out.push(ui(lang, "ui.input.task.without", { task: id, files: files.join(", ") }));
115
+ }
116
+ else if (o.op === "task.drop" && id)
117
+ out.push(ui(lang, "ui.input.task.dropped", { task: id, files: (before.get(id) ?? []).join(", ") }));
118
+ else if (o.op === "input.hide" && id) {
119
+ const why = tree.get(id)?.hidden?.reason;
120
+ out.push(`${ui(lang, "ui.input.hidden", { id })}${why ? ` (${ui(lang, `ui.input.why.${why}`)})` : ""}`);
121
+ }
122
+ else if (o.op === "input.unskip" && id)
123
+ out.push(ui(lang, tree.get(id)?.state === "processed" ? "ui.input.shown.processed" : "ui.input.shown.new", { id }));
124
+ }
125
+ return out;
126
+ }
64
127
  function researchFor(tree, named) {
65
128
  if (typeof named === "string")
66
129
  return requireRecord(tree, named, "research").id;
@@ -340,8 +403,10 @@ register({
340
403
  run(ctx, { args, opts }) {
341
404
  const tree = ctx.tree();
342
405
  const ids = idsGiven(ctx, args, opts);
406
+ const before = filesOfTasks(tree);
343
407
  const got = tree.withTreeLock(() => unhideMaterial(tree, ids));
344
- return { text: got.changed.length ? lines(...tree.written.map((o) => o.summary)) : "nothing skipped: nothing changed", data: got };
408
+ const tasks = got.tasks.map((id) => ui(tree.lang, "ui.input.task.gather", { task: id, what: tree.get(id)?.what ?? "" }));
409
+ return { text: got.changed.length ? lines(...materialLines(tree, before), ...tasks) : ui(tree.lang, "ui.input.unskip.none"), data: got };
345
410
  },
346
411
  }, {
347
412
  path: ["input", "hide"],
@@ -364,8 +429,12 @@ register({
364
429
  if (why !== undefined && !HIDE_REASONS.includes(why))
365
430
  throw new UsageError(`--why: ${HIDE_REASONS.join(" or ")}`, { code: "material.body", params: { why: why.slice(0, 40) } });
366
431
  const ids = idsGiven(ctx, args, opts);
432
+ const before = filesOfTasks(tree);
367
433
  const got = tree.withTreeLock(() => hideMaterial(tree, ids, { reason: why, by: fileBy(tree, ctx.env) }));
368
- return { text: got.changed.length ? lines(...tree.written.map((o) => o.summary), `show again: strom input unskip ${got.changed.length > 3 ? "…" : got.changed.join(" ")}`) : "hidden already: nothing changed", data: got };
434
+ return {
435
+ text: got.changed.length ? lines(...materialLines(tree, before), ui(tree.lang, "ui.input.hide.back", { ids: got.changed.length > 3 ? "…" : got.changed.join(" ") })) : ui(tree.lang, "ui.input.hide.none"),
436
+ data: got,
437
+ };
369
438
  },
370
439
  }, {
371
440
  path: ["input", "assign"],
@@ -393,7 +462,7 @@ register({
393
462
  throw new UsageError("give the files or --batch, not both", { code: "material.body" });
394
463
  const got = tree.withTreeLock(() => assignMaterial(tree, { ...(batch ? { batch } : { files: idsGiven(ctx, args, opts) }), add: people(opts.add), remove: people(opts.remove) }));
395
464
  return {
396
- text: lines(got.changed.length ? lines(...tree.written.map((o) => o.summary)) : "nothing to change", ...got.kept.map((k) => `${k.id}: ${k.person} stays — the whole batch is theirs (strom input assign --batch ${k.batch} --remove ${k.person})`)),
465
+ text: lines(got.changed.length ? lines(...materialLines(tree, new Map(), got.batch)) : ui(tree.lang, "ui.input.assign.none"), ...got.kept.map((k) => ui(tree.lang, "ui.input.assign.kept", { id: k.id, who: whoOf(tree, [k.person]), batch: k.batch, person: k.person }))),
397
466
  data: got,
398
467
  };
399
468
  },
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.20.0";
32
+ export const VERSION = "1.21.0-beta.1";
33
33
  const GITIGNORE = `# Strom Research — local state and large files are not versioned
34
34
  .strom/
35
35
  /media/
@@ -1,7 +1,8 @@
1
1
  // What waits for the person, in one ordered list: the tasks that wait for them, the questions about archives, the new
2
2
  // versions of approved stories — and, for the terminal, the trees the Strom app sent and the questions about the reading
3
3
  // of scans. The bridge sends it to the Strom app (/status `waiting`, the app's spec ZADANI_VYZKUM_menu-ceka.md); the
4
- // menu's What waits reads the same. Computed, nothing written.
4
+ // menu's What waits reads the same — the terminal's own marked `terminalOnly` (TERMINAL_ONLY), so the menu says what
5
+ // of its count the app does not show (waitingSplit, waitingLine). Computed, nothing written.
5
6
  //
6
7
  // Each item: its kind (core/waitkind.ts for a task), the section the app shows it in, the place where it is dealt with
7
8
  // (its group), todo or sent, its order; for a task its priority, the site, the request's text, whom, the folder for
@@ -66,6 +67,35 @@ function actionsOf(kind, phase, note) {
66
67
  : [];
67
68
  return all.filter((a) => ACTIONS_OFFERED.includes(a));
68
69
  }
70
+ /**
71
+ * The options that add what only the terminal shows (the menu's What waits): the bridge never asks for them, so the
72
+ * Strom app never shows them. Each item they add is marked `terminalOnly`; a new kind of the terminal's own goes here.
73
+ */
74
+ export const TERMINAL_ONLY = ["sends", "tune", "conflicts", "milder"];
75
+ /** Whether the Strom app shows a thing that waits too (its /status `waiting`): everything but the terminal's own. */
76
+ export function inApp(w) {
77
+ return !w.terminalOnly;
78
+ }
79
+ /**
80
+ * How many things wait, split as the Strom app counts them: all of them (the menu's count), those shown only in the
81
+ * terminal, those in the app that wait for a reply (sent — the app counts only what is to do), and what is to do in
82
+ * the app (its number: all − only here − sent).
83
+ */
84
+ export function waitingSplit(items) {
85
+ const here = items.filter((w) => !inApp(w)).length;
86
+ const sent = items.filter((w) => inApp(w) && w.item.phase === "sent").length;
87
+ return { all: items.length, here, sent, app: items.length - here - sent };
88
+ }
89
+ /**
90
+ * The line "Waiting: N" for the person: what of it is only here in the terminal and what waits for a reply, each said
91
+ * on its own — the rest is the app's number; as it was only when neither is there (N is the app's number then).
92
+ */
93
+ export function waitingLine(lang, items, key = "ui.menu.waiting") {
94
+ const s = waitingSplit(items);
95
+ const parts = [...(s.here ? [ui(lang, "ui.waiting.split.here", { n: s.here })] : []), ...(s.sent ? [ui(lang, "ui.waiting.split.reply", { n: s.sent })] : [])];
96
+ const count = parts.length ? ui(lang, "ui.waiting.split", { n: s.all, parts: parts.join(", ") }) : String(s.all);
97
+ return ui(lang, key, { count });
98
+ }
69
99
  /** An address a person may open: http(s) only. */
70
100
  function webUrl(u) {
71
101
  return u && /^https?:\/\//iu.test(u) && URL.canParse(u) ? u : undefined;
@@ -256,7 +286,7 @@ export function waitingFor(tree, o = {}) {
256
286
  for (const q of questions.filter((p) => p.kind === "milder" && p.host && p.connector)) {
257
287
  const title = q.title ?? q.connector;
258
288
  const what = ui(lang, "ui.archive.milder.item", { title, host: q.host, to: levelWords(lang, q.to ?? "allowed"), from: levelWords(lang, q.from ?? "restricted") });
259
- out.push({ question: q, item: { id: q.key, what, on: "", at: q.at, kind: "milder", section: "decide", group: archiveGroup(tree, q.host, title), phase: "todo", actions: [], host: q.host, connector: q.connector } });
289
+ out.push({ question: q, terminalOnly: true, item: { id: q.key, what, on: "", at: q.at, kind: "milder", section: "decide", group: archiveGroup(tree, q.host, title), phase: "todo", actions: [], host: q.host, connector: q.connector } });
260
290
  }
261
291
  // a new version of an approved story: whose (a couple's: person and partner), since when
262
292
  if (o.stories !== false)
@@ -291,6 +321,7 @@ export function waitingFor(tree, o = {}) {
291
321
  for (const r of receivedPending(tree.root))
292
322
  out.push({
293
323
  send: r,
324
+ terminalOnly: true,
294
325
  item: {
295
326
  id: r.intake,
296
327
  what: ui(lang, "ui.waiting.sent", { day: humanDay(r.at, lang), time: r.at.slice(11, 16), n: r.changes }),
@@ -308,6 +339,7 @@ export function waitingFor(tree, o = {}) {
308
339
  for (const c of conflictsToDecide(tree))
309
340
  out.push({
310
341
  conflict: c,
342
+ terminalOnly: true,
311
343
  item: { id: c.id, what: conflictTitle(tree, c), on: "", at: c.created, kind: "conflict", section: "decide", group: { key: "conflicts", name: phrase(lang, "wait.group.conflicts"), kind: "other" }, phase: "todo", actions: [] },
312
344
  });
313
345
  // the questions about the reading of scans only a person answers
@@ -315,6 +347,7 @@ export function waitingFor(tree, o = {}) {
315
347
  for (const q of tuneQuestions(tree, o.tune).due)
316
348
  out.push({
317
349
  tune: q,
350
+ terminalOnly: true,
318
351
  item: { id: q.id, what: q.text, on: q.why, at: q.asked, kind: "tune", section: "decide", group: { key: "tune", name: phrase(lang, "wait.group.tune"), kind: "other" }, phase: "todo", actions: [] },
319
352
  });
320
353
  // one place, one name: the first said of it (an archive's question names it as the research knows the archive; its
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strom-research",
3
- "version": "1.20.0",
3
+ "version": "1.21.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
  }