strom-research 1.5.0 → 1.6.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.
@@ -158,6 +158,14 @@ async function newResearch(ctx, run, lang, root) {
158
158
  return;
159
159
  who = [`--new-person=${name}`, "--sex", ["M", "F", "U"][sex]];
160
160
  }
161
+ await startDirection(ctx, run, lang, root, direction, who, name, question);
162
+ }
163
+ /**
164
+ * A new direction for someone chosen (the menu, a link of the Strom app): its title and what the person knows of
165
+ * it asked, the research made; the same research still going is only named.
166
+ */
167
+ export async function startDirection(ctx, run, lang, root, direction, who, name, question) {
168
+ const t = translator(lang);
161
169
  // The same research again (this person, this direction, still going): said which one it is, nothing made.
162
170
  const focus = who[0] === "--person" ? who[1] : undefined;
163
171
  const same = focus && direction !== "question"
@@ -198,15 +206,8 @@ async function syncTree(ctx, run, lang, root) {
198
206
  const from = await ctx.choose(t("ui.sync.from"), [{ label: t("ui.sync.from.app") }, { label: t("ui.sync.from.file") }], 0, { back: t("ui.browse.back") });
199
207
  if (from === undefined)
200
208
  return;
201
- if (from === 0) {
202
- const since = Date.now();
203
- if ((await run(["sync", "--app"])) !== 0)
204
- return pause(ctx, lang);
205
- file = receivedSince(root, since);
206
- if (!file)
207
- return pause(ctx, lang);
208
- return confirmSync(ctx, run, lang, root, file);
209
- }
209
+ if (from === 0)
210
+ return syncFromApp(ctx, run, lang, root);
210
211
  }
211
212
  for (;;) {
212
213
  if (outOfAnswers(ctx))
@@ -234,8 +235,21 @@ async function syncTree(ctx, run, lang, root) {
234
235
  return pause(ctx, lang);
235
236
  return confirmSync(ctx, run, lang, root, file, count);
236
237
  }
238
+ /** The tree straight from the Strom app (the menu, or a strom-research://send link): shown, then written on the person's word. */
239
+ export async function syncFromApp(ctx, run, lang, root, enter = "ui.enter", back) {
240
+ const since = Date.now();
241
+ const code = await run(["sync", "--app"]);
242
+ // the person is in the app: what came of it is here (a terminal opened for a link brings itself forward)
243
+ back?.();
244
+ if (code !== 0)
245
+ return pause(ctx, lang, enter);
246
+ const file = receivedSince(root, since);
247
+ if (!file)
248
+ return pause(ctx, lang, enter);
249
+ return confirmSync(ctx, run, lang, root, file, undefined, enter);
250
+ }
237
251
  /** What the tree brings is shown: written on the person's word — all, or the ones they pick. */
238
- async function confirmSync(ctx, run, lang, root, file, known) {
252
+ async function confirmSync(ctx, run, lang, root, file, known, enter = "ui.enter") {
239
253
  const t = translator(lang);
240
254
  let count = known ?? 0;
241
255
  if (known === undefined)
@@ -247,7 +261,7 @@ async function confirmSync(ctx, run, lang, root, file, known) {
247
261
  count = 0;
248
262
  }
249
263
  if (!count)
250
- return pause(ctx, lang);
264
+ return pause(ctx, lang, enter);
251
265
  const how = await ctx.choose(t("ui.sync.how"), [{ label: t("ui.sync.all") }, { label: t("ui.sync.some") }], 0, { back: t("ui.browse.back") });
252
266
  if (how === undefined)
253
267
  return;
@@ -260,7 +274,7 @@ async function confirmSync(ctx, run, lang, root, file, known) {
260
274
  }
261
275
  if ((await run(["sync", file, "--apply", ...only])) === 0 && !noApp(ctx))
262
276
  ctx.io.stdout(t("ui.sync.app") + "\n");
263
- await pause(ctx, lang);
277
+ await pause(ctx, lang, enter);
264
278
  }
265
279
  /** The people no record of their own proves: listed, then all of them a batch at a time, or the ones picked by number. */
266
280
  async function reviewUnproven(ctx, run, lang, root) {
@@ -342,12 +356,17 @@ async function reviewPerson(ctx, run, lang, root) {
342
356
  if (!person)
343
357
  return;
344
358
  const family = await ctx.confirm(t("ui.review.family"), false);
345
- if ((await run(["review", person.id, ...(family ? ["--scope", "family"] : [])])) !== 0)
346
- return pause(ctx, lang);
359
+ await reviewOne(ctx, run, lang, root, person.id, family ? "family" : "person");
360
+ await pause(ctx, lang);
361
+ }
362
+ /** One person's review (the menu, a link of the Strom app), then the agent offered for its tasks — the person's choice, no suggested. */
363
+ export async function reviewOne(ctx, run, lang, root, person, scope) {
364
+ const t = translator(lang);
365
+ if ((await run(["review", person, ...(scope !== "person" ? ["--scope", scope] : [])])) !== 0)
366
+ return;
347
367
  const tree = Tree.open(root, ctx.env);
348
- const research = tree.list("research").find((r) => r.direction === "person" && r.focus === person.id && r.review && !r.review.people);
368
+ const research = tree.list("research").find((r) => r.direction === "person" && r.focus === person && r.review && !r.review.people);
349
369
  const open = research ? tree.list("task").filter((x) => x.research === research.id && x.state === "open").length : 0;
350
370
  if (research && open && (await ctx.confirm(t("ui.review.run", { n: open }), false)))
351
371
  await run(["run", "--research", research.id, "--max", String(open)]);
352
- await pause(ctx, lang);
353
372
  }
@@ -12,11 +12,63 @@ import { openForUser } from "../core/open.js";
12
12
  const SHOWN = 9;
13
13
  /** An answer is a note on the task (500 characters, with what it asked). */
14
14
  const ANSWER_MAX = 300;
15
- export async function waitingForYou(ctx, run, lang, root) {
15
+ /** What to do for a task that waits: the images to save and where, or what it asks. */
16
+ function whatToDo(ctx, lang, tree, x) {
16
17
  const t = translator(lang);
17
- const out = (line) => ctx.io.stdout(line + "\n");
18
+ if (!x.awaits)
19
+ return [` ${t("ui.waiting.do", { what: x.waitingOn || t("ui.waiting.ask") })}`];
20
+ const b = tree.get(x.awaits.recordset);
21
+ return [
22
+ ` ${t("ui.waiting.images", { images: x.awaits.images, book: b?.title ?? x.awaits.recordset })}${b?.url ? ` · ${b.url}` : ""}`,
23
+ ` ${t("ui.wait.save", { dir: `${ctx.display(inbox(ctx, x) ?? `inbox/${x.awaits.folder}`)}${path.sep}` })}`,
24
+ ];
25
+ }
26
+ function inbox(ctx, x) {
18
27
  const shared = ctx.settings.shared()?.value;
19
- const inbox = (x) => (shared && x.awaits ? path.join(shared, "inbox", x.awaits.folder) : undefined);
28
+ return shared && x.awaits ? path.join(shared, "inbox", x.awaits.folder) : undefined;
29
+ }
30
+ /** One task that waits, taken up: its folder opened for the images, or the person's answer and the task back in the queue. */
31
+ async function takeUp(ctx, run, lang, root, x) {
32
+ const t = translator(lang);
33
+ const out = (line) => ctx.io.stdout(line + "\n");
34
+ if (x.awaits) {
35
+ const dir = inbox(ctx, x);
36
+ if (dir)
37
+ openForUser(dir, ctx.env);
38
+ return void out(t("ui.waiting.opened", { dir: dir ? ctx.display(dir) : `inbox/${x.awaits.folder}` }));
39
+ }
40
+ let answer = "";
41
+ for (;;) {
42
+ answer = (await ctx.ask(t("ui.waiting.write"))).trim();
43
+ if (!answer || answer === "0")
44
+ return;
45
+ // kept as a note on the task, with what it asked: short — a longer story is for a conversation
46
+ if (answer.length <= ANSWER_MAX)
47
+ break;
48
+ out(t("ui.waiting.long", { n: ANSWER_MAX }));
49
+ if (ctx.io.answers !== undefined && ctx.io.answers.length === 0)
50
+ return;
51
+ }
52
+ // meanwhile taken up by an agent (done, dropped, back in the queue): said, nothing written
53
+ if (Tree.open(root, ctx.env).get(x.id)?.state !== "waiting")
54
+ return void out(t("ui.waiting.gone"));
55
+ if ((await run(["task", "wake", x.id, `--answer=${answer}`], true)) !== 0)
56
+ return;
57
+ out(t("ui.waiting.back"));
58
+ await offerChat(ctx, run, lang, root, t("ui.waiting.chat"), t("ui.waiting.say", { what: truncate(x.what, 80) }));
59
+ }
60
+ /** One task that waits, named by a link of the Strom app: what it asks, then taken up as in the menu. False: it waits no more. */
61
+ export async function waitingTask(ctx, run, lang, root, id) {
62
+ const tree = Tree.open(root, ctx.env);
63
+ const x = tree.get(id);
64
+ if (!x || x.state !== "waiting")
65
+ return false;
66
+ ctx.io.stdout([` ${x.what}`, ...whatToDo(ctx, lang, tree, x), ...(x.awaits ? ["", translator(lang)("ui.wait.how1"), translator(lang)("ui.wait.how2")] : []), ""].join("\n") + "\n");
67
+ await takeUp(ctx, run, lang, root, x);
68
+ return true;
69
+ }
70
+ export async function waitingForYou(ctx, run, lang, root) {
71
+ const t = translator(lang);
20
72
  await subMenu(ctx, lang, () => {
21
73
  const tree = Tree.open(root, ctx.env);
22
74
  const waiting = tree.list("task").filter((x) => x.state === "waiting");
@@ -25,55 +77,17 @@ export async function waitingForYou(ctx, run, lang, root) {
25
77
  const shown = waiting.slice(0, SHOWN);
26
78
  const text = [t("ui.waiting.title", { n: waiting.length })];
27
79
  for (const [k, x] of shown.entries()) {
28
- text.push("", ` ${k + 1}. ${x.what}`);
29
- if (x.awaits) {
30
- const b = tree.get(x.awaits.recordset);
31
- text.push(` ${t("ui.waiting.images", { images: x.awaits.images, book: b?.title ?? x.awaits.recordset })}${b?.url ? ` · ${b.url}` : ""}`);
32
- text.push(` ${t("ui.wait.save", { dir: `${ctx.display(inbox(x) ?? `inbox/${x.awaits.folder}`)}${path.sep}` })}`);
33
- }
34
- else
35
- text.push(` ${t("ui.waiting.do", { what: x.waitingOn || t("ui.waiting.ask") })}`);
80
+ text.push("", ` ${k + 1}. ${x.what}`, ...whatToDo(ctx, lang, tree, x));
36
81
  }
37
82
  if (shown.some((x) => x.awaits))
38
83
  text.push("", t("ui.wait.how1"), t("ui.wait.how2"));
39
84
  if (waiting.length > SHOWN)
40
85
  text.push("", t("ui.waiting.more", { n: waiting.length - SHOWN }));
41
- const items = shown.map((x, k) => x.awaits
42
- ? {
43
- key: String(k + 1),
44
- label: t("ui.waiting.folder", { k: k + 1 }),
45
- act: async () => {
46
- const dir = inbox(x);
47
- if (dir)
48
- openForUser(dir, ctx.env);
49
- out(t("ui.waiting.opened", { dir: dir ? ctx.display(dir) : `inbox/${x.awaits.folder}` }));
50
- },
51
- }
52
- : {
53
- key: String(k + 1),
54
- label: t("ui.waiting.answer", { k: k + 1, what: truncate(x.what, 60) }),
55
- act: async () => {
56
- let answer = "";
57
- for (;;) {
58
- answer = (await ctx.ask(t("ui.waiting.write"))).trim();
59
- if (!answer || answer === "0")
60
- return;
61
- // kept as a note on the task, with what it asked: short — a longer story is for a conversation
62
- if (answer.length <= ANSWER_MAX)
63
- break;
64
- out(t("ui.waiting.long", { n: ANSWER_MAX }));
65
- if (ctx.io.answers !== undefined && ctx.io.answers.length === 0)
66
- return;
67
- }
68
- // meanwhile taken up by an agent (done, dropped, back in the queue): said, nothing written
69
- if (Tree.open(root, ctx.env).get(x.id)?.state !== "waiting")
70
- return void out(t("ui.waiting.gone"));
71
- if ((await run(["task", "wake", x.id, `--answer=${answer}`], true)) !== 0)
72
- return;
73
- out(t("ui.waiting.back"));
74
- await offerChat(ctx, run, lang, root, t("ui.waiting.chat"), t("ui.waiting.say", { what: truncate(x.what, 80) }));
75
- },
76
- });
86
+ const items = shown.map((x, k) => ({
87
+ key: String(k + 1),
88
+ label: x.awaits ? t("ui.waiting.folder", { k: k + 1 }) : t("ui.waiting.answer", { k: k + 1, what: truncate(x.what, 60) }),
89
+ act: () => takeUp(ctx, run, lang, root, x),
90
+ }));
77
91
  return { title: `${text.join("\n")}\n\n${t("ui.waiting.pick")}`, items };
78
92
  });
79
93
  }
package/dist/cli/ui.js CHANGED
@@ -7,6 +7,7 @@ export const UI = {
7
7
  "ui.yes": "y",
8
8
  "ui.no": "n",
9
9
  "ui.enter": "Press Enter to go back to the menu.",
10
+ "ui.enter.close": "Press Enter to close this window.",
10
11
  "ui.choose": "Choose",
11
12
  "ui.bad.choice": "Please type one of the numbers.",
12
13
  "ui.dialog.title": "Strom research",
@@ -88,6 +89,8 @@ export const UI = {
88
89
  "ui.setup.level.sure": "Really allow everything?",
89
90
  "ui.setup.shortcut": "Put a \"Strom research\" shortcut on the desktop?",
90
91
  "ui.setup.shortcut.done": "✓ Shortcut: {file}",
92
+ "ui.setup.links": "Let the Strom app start the research on this computer? (One click sends your edits back, opens an excerpt in full quality, the agent or a person's review — each asked here first.)",
93
+ "ui.setup.links.done": "✓ Links from the Strom app lead to the research here.",
91
94
  "ui.setup.skill": "✓ {agent} now knows strom in any folder.",
92
95
  "ui.setup.app": "✓ The Strom app is on this computer — the menu opens the research in it, and while the agent works the app follows it live.",
93
96
  "ui.setup.done": "All set.",
@@ -177,6 +180,10 @@ export const UI = {
177
180
  "ui.doc.level": "the agent may",
178
181
  "ui.doc.model": "model",
179
182
  "ui.doc.shortcut": "desktop shortcut",
183
+ "ui.doc.links": "links from the Strom app",
184
+ "ui.doc.links.ours": "yes",
185
+ "ui.doc.links.other": "lead to another program (or where strom was before)",
186
+ "ui.doc.links.none": "none",
180
187
  "ui.doc.app": "Strom app",
181
188
  "ui.doc.tree": "this family tree",
182
189
  "ui.doc.innode": "strom {version} in Node {node}",
@@ -266,6 +273,7 @@ export const UI = {
266
273
  "ui.setup.stories.no": "No — just the family tree and its sources",
267
274
  "ui.uninstall.agent": "what {agent} knows of strom",
268
275
  "ui.uninstall.shortcut": "the shortcut",
276
+ "ui.uninstall.links": "the links from the Strom app",
269
277
  "ui.uninstall.git": "strom's own git",
270
278
  "ui.uninstall.path": "the installer's PATH entry",
271
279
  "ui.uninstall.program": "the program",
@@ -412,10 +420,13 @@ export const UI = {
412
420
  "ui.sync.child.new": "{n}. {who}: a child — {child}",
413
421
  "ui.sync.child.gone": "{n}. {who}: no longer their child — {child}",
414
422
  "ui.sync.person.gone": "{n}. no longer in it: {name}",
423
+ "ui.sync.place.coords": "{n}. {place}: on the map {from} → {at}",
424
+ "ui.sync.place.located": "{n}. {place}: on the map at {at}",
415
425
  "ui.sync.do.add": "added as a lead",
416
426
  "ui.sync.do.correct": "corrects the research's lead",
417
427
  "ui.sync.do.conflict": "a conflict for you to decide (the research has it from a record)",
418
428
  "ui.sync.do.user": "your edit wins (the record's fact withdrawn with the reason)",
429
+ "ui.sync.do.set": "the research takes your position (no record proves one)",
419
430
  "ui.sync.do.pick": "only when you pick it (--only)",
420
431
  "ui.sync.do.report": "nothing changes — tell the agent what you meant",
421
432
  "ui.sync.edits": "Your edits to facts a record proves: {mode} (strom config set sync.edits conflict|user)",
@@ -430,6 +441,7 @@ export const UI = {
430
441
  "ui.sync.bridge.foreign": "this does not look like the research's family tree — export it and load it in the research as another family tree",
431
442
  "ui.sync.bridge.empty": "the tree came empty or unreadable",
432
443
  "ui.sync.wait": "The Strom app opens with this research — choose which tree to send, and confirm there. Waiting for it (at most {min} min; Ctrl+C stops) …",
444
+ "ui.sync.wait.open": "Nothing shows in the app (it was open already)? Open this address in its window's browser: {url}",
433
445
  "ui.sync.waited": "The Strom app did not send the tree within {min} min — try again: strom sync --app, or export it as GEDCOM: strom sync <file>",
434
446
  "ui.sync.got": "The Strom app sent the tree.",
435
447
  "ui.sync.nothing.unchanged": "The Strom app sends nothing: the tree has not changed since it was last loaded from the research.",
@@ -512,6 +524,79 @@ export const UI = {
512
524
  "ui.more.intake": "Material from the family: documents, photos, a family tree from another program, what you know",
513
525
  "ui.more.research": "A new direction: the ancestors or descendants of someone else, or one question",
514
526
  "ui.more.sync": "Take in edits from the Strom app or another family tree (.ged, .json)",
527
+ "ui.link.ok": "OK",
528
+ "ui.link.bad": "This link from the Strom app could not be used ({why}). Nothing was done.",
529
+ "ui.link.notree": "The research this link is for is not on this computer (or strom does not know its folder). Nothing was done.",
530
+ "ui.link.which": "This research is here more than once. Which one should take the edits?",
531
+ "ui.link.noclip": "This excerpt of {source} is no longer in the research (it was made again or taken away). Open the Strom app from the research again to get the current ones.",
532
+ "ui.link.noscan": "The scan of this excerpt of {source} is not on this computer, and the research knows no page of it online.",
533
+ "ui.link.noterminal": "strom could not open a terminal window. Start strom and choose: {item} — Straight from the Strom app.",
534
+ "ui.link.on.done": "✓ Links from the Strom app lead to the research here.",
535
+ "ui.link.on.failed": "The links from the Strom app could not be set up on this computer.",
536
+ "ui.link.off.done": "Links from the Strom app no longer lead to the research here.",
537
+ "ui.link.off.failed": "The links from the Strom app could not be taken off this computer.",
538
+ "ui.link.which.one": "This research is here more than once. Which one?",
539
+ "ui.link.noterminal.any": "strom could not open a terminal window. Start strom (the research {name}) and go on from its menu.",
540
+ "ui.link.noterminal.new": "strom could not open a terminal window. Start strom, then try again from the Strom app.",
541
+ "ui.link.asks": "The Strom app asks for: {what}\nResearch: {name}",
542
+ "ui.link.go": "Go on?",
543
+ "ui.link.cost": "The agent works on your subscription or AI credit.",
544
+ "ui.link.tasks": "The research writes tasks for the agent — nothing is searched and nothing is paid now.",
545
+ "ui.link.what.chat": "a conversation with the agent ({agent}) about the research.",
546
+ "ui.link.what.chat.person": "a conversation with the agent ({agent}) about {person}.",
547
+ "ui.link.chat.say": "Let us look at {person}: tell me briefly what the research knows of this person, and ask me what I want to find out.",
548
+ "ui.link.what.review.person": "look at {person} again.",
549
+ "ui.link.what.review.family": "look at {person} again, with the partners and children.",
550
+ "ui.link.what.review.line": "look at {person} again, with the ancestors.",
551
+ "ui.link.what.research.ancestors": "search for the ancestors of {person} (a new direction of the research).",
552
+ "ui.link.what.research.descendants": "search for the descendants of {person} (a new direction of the research).",
553
+ "ui.link.noperson": "{person} is not in this research (or was taken out of it). Nothing was done.",
554
+ "ui.link.notask": "This task no longer waits for you (it was answered, or an agent took it up). Nothing was done.",
555
+ "ui.link.task.none": "Task {task} is not in this research. Nothing was done.",
556
+ "ui.link.task.cannot": "This task cannot be changed that way now (it changed meanwhile). Nothing was done.",
557
+ "ui.link.what.task.park": "put the task aside: {what}",
558
+ "ui.link.what.task.drop": "give up the task: {what}",
559
+ "ui.link.what.task.wake": "bring the task back into the queue: {what}",
560
+ "ui.link.task.why": "Why? (a few words, kept on the task)",
561
+ "ui.link.task.park.default": "Put aside in the Strom app.",
562
+ "ui.link.task.drop.default": "Given up in the Strom app.",
563
+ "ui.link.task.park.done": "✓ The task is put aside.",
564
+ "ui.link.task.drop.done": "✓ The task is given up (kept, with the reason).",
565
+ "ui.link.task.wake.done": "✓ The task is back in the agent's queue.",
566
+ "ui.link.sessions.title": "The agent's sessions — {name}",
567
+ "ui.link.sessions.none": "The agent has not worked on this research yet.",
568
+ "ui.link.sessions.month": "This month: {n} sessions, {cost}",
569
+ "ui.link.sessions.more": "… and {n} earlier",
570
+ "ui.link.conflict.none": "Conflict {id} is not in this research. Nothing was done.",
571
+ "ui.link.conflict.decided": "This conflict is decided already: {resolution}",
572
+ "ui.link.what.conflict.decide": "decide the conflict: {title}",
573
+ "ui.link.what.conflict.agent": "leave the conflict to the agent ({agent}): {title}",
574
+ "ui.link.conflict.pick": "Which is right?",
575
+ "ui.link.conflict.other": "Something else (I write it)",
576
+ "ui.link.conflict.own": "What is right?",
577
+ "ui.link.conflict.why": "Why? (what decides it)",
578
+ "ui.link.conflict.by": "{why} (the user's decision)",
579
+ "ui.link.conflict.done": "✓ The conflict is decided.",
580
+ "ui.link.conflict.say": "Look at the conflict {id} ({title}): weigh the records, decide it with strom conflict resolve, then tell me what you concluded and why.",
581
+ "ui.link.story.none": "{person} has no story yet. Nothing was done.",
582
+ "ui.link.story.final": "The story of {person} is approved already.",
583
+ "ui.link.what.story": "approve the story of {person} as it is (it will no longer be a draft).",
584
+ "ui.link.story.done": "✓ The story is approved. Open the research in the Strom app again and it is there without the draft label.",
585
+ "ui.link.sync.none": "{input} is not a sending from the Strom app in this research. Nothing was done.",
586
+ "ui.link.sync.was": "This sending was taken back already.",
587
+ "ui.link.what.sync": "take back what the research took from the Strom app on {day} ({n} changes).",
588
+ "ui.link.new.title": "The Strom app asks to start a research with one of its family trees.",
589
+ "ui.link.new.name": "Name of the research (the family, e.g. Novák)",
590
+ "ui.link.new.exists": "A research named “{name}” is here already, with people in it. Choose another name.",
591
+ "ui.link.new.wait": "Waiting for the Strom app to hand the tree over — confirm it there (at most {min} min; Ctrl+C stops).",
592
+ "ui.link.new.none": "The Strom app handed no tree over. The research “{name}” is here, still empty: start again from the app, or add to it from strom's menu.",
593
+ "ui.link.new.waited": "No tree came from the Strom app within {min} min. The research “{name}” is here, still empty.",
594
+ "ui.link.new.empty": "The tree the Strom app sent has no people, so there is nothing to research. The research “{name}” is here, still empty.",
595
+ "ui.link.new.failed": "The Strom app could not hand the tree over. The research “{name}” is here, still empty: try again from the app.",
596
+ "ui.link.new.taken": "✓ The family tree is in the research “{name}”: {persons} people, {families} families — as leads; what records prove, the agent finds.",
597
+ "ui.link.new.back": "The Strom app shows the tree now as the tree of the research.",
598
+ "ui.link.new.chat": "Start the conversation with the agent now?",
599
+ "ui.link.new.say": "I handed our family tree over from the Strom app — look at what is in it and suggest where to begin.",
515
600
  "ui.more.sync.noapp": "Take in another family tree's changes (.ged, .json)",
516
601
  "ui.sync.path.noapp": "Drag the family tree file (.ged or .json) here, or type its path (Enter or 0 goes back)",
517
602
  "ui.sync.path": "Drag the file exported from the Strom app (.ged or .json) here, or type its path (Enter or 0 goes back)",
@@ -13,9 +13,10 @@ import { agentsHere, DESKTOP_APPS } from "../core/apps.js";
13
13
  import { PROFILES } from "../agents/profiles.js";
14
14
  import { writeStored, PERMISSION_LEVELS } from "../core/config.js";
15
15
  import { globalTargets, installGlobal } from "../agents/global.js";
16
- import { noticeStromApp, researchUrl, stromAppState, stromAppUrl } from "../core/stromapp.js";
16
+ import { appOpensLinks, noticeStromApp, researchUrl, stromAppState, stromAppUrl } from "../core/stromapp.js";
17
17
  import { openForUser } from "../core/open.js";
18
18
  import { createShortcut } from "../core/shortcut.js";
19
+ import { linkFiles, linkHandlerState, registerLinks } from "../core/links.js";
19
20
  import { desktopDir } from "../core/paths.js";
20
21
  import { ensureShared } from "../commands/setup.js";
21
22
  import { moveHome, planMove, repointSettings, sameFolder } from "../core/relocate.js";
@@ -152,10 +153,42 @@ export async function setupWizard(ctx) {
152
153
  out(ui(lang, "ui.setup.app"));
153
154
  else
154
155
  await askStromApp(ctx, lang, first ? undefined : ui(lang, "ui.keep"));
156
+ // 9. The Strom app may start the research here (strom-research:// links): asked while it is wanted; refreshed quietly after.
157
+ if (stromAppState(s) !== "no" && appOpensLinks(s))
158
+ await offerLinks(ctx, lang);
155
159
  out();
156
160
  out(`${ui(lang, "ui.setup.done")} (${langName(lang, lang)} · ${ctx.display(home)})`);
157
161
  return { home, lang, ...(agent ? { agent } : {}), permissions: level };
158
162
  }
163
+ /**
164
+ * strom-research:// links: set up on the person's yes, the answer kept (a no is not asked again unasked:
165
+ * `ask` — the wizard, doctor --fix — asks all the same, suggesting what they said). Set up before by strom: set up
166
+ * again quietly.
167
+ */
168
+ export async function offerLinks(ctx, lang, opts = { ask: true }) {
169
+ if (linkHandlerState(ctx.env) === "ours")
170
+ return;
171
+ const said = ctx.settings.config.links;
172
+ const made = linkFiles(ctx.env).length > 0;
173
+ if (!made) {
174
+ if (said === "no" && !opts.ask)
175
+ return;
176
+ const yes = await ctx.confirm(ui(lang, "ui.setup.links"), said !== "no");
177
+ ctx.settings.reload();
178
+ ctx.settings.config.links = yes ? "yes" : "no";
179
+ ctx.settings.save();
180
+ if (!yes)
181
+ return;
182
+ }
183
+ let done = false;
184
+ try {
185
+ done = registerLinks(ctx.env);
186
+ }
187
+ catch {
188
+ done = false;
189
+ }
190
+ ctx.io.stdout(ui(lang, done ? "ui.setup.links.done" : "ui.link.on.failed") + "\n");
191
+ }
159
192
  /**
160
193
  * Does the person want the Strom app (not found on this computer)? Yes and
161
194
  * install it now: it opens in the browser and strom says where to click; yes
@@ -261,9 +261,10 @@ register({
261
261
  options: [
262
262
  { name: "about", type: "string", multiple: true, value: "<who>", description: "person/record concerned (repeatable)" },
263
263
  { name: "claim", type: "string", multiple: true, value: "<S…: value>", description: 'a claim, e.g. "S0027: aged 27 at marriage 1839" (repeat for each)' },
264
+ { name: "fact", type: "string", value: "<TAG>", description: "the fact it is about, by its GEDCOM tag: BIRT, DEAT, BAPM, BURI, MARR, NAME, SEX… (the Strom app shows it there)" },
264
265
  { name: "note", type: "string", value: "<text>", description: "short note" },
265
266
  ],
266
- examples: ['strom conflict add "Rok narození Josefa" --about P0002 --claim "S0001: 27 let při sňatku 1839" --claim "S0002: 70 let při úmrtí 1883"'],
267
+ examples: ['strom conflict add "Rok narození Josefa" --about P0002 --fact BIRT --claim "S0001: 27 let při sňatku 1839" --claim "S0002: 70 let při úmrtí 1883"'],
267
268
  run(ctx, { args, opts }) {
268
269
  const tree = ctx.tree();
269
270
  const claims = listOpt(opts.claim).map((c) => {
@@ -277,7 +278,10 @@ register({
277
278
  const subject = anyRefs(tree, listOpt(opts.about));
278
279
  if (!subject.length)
279
280
  throw new UsageError("--about is required");
280
- const x = create(tree, "conflict", { title: args[0].trim(), subject, claims, state: "open", note: opts.note }, (id) => `+${id} conflict "${truncate(args[0], 60)}"`, subject);
281
+ const fact = typeof opts.fact === "string" ? opts.fact.trim().toUpperCase() : undefined;
282
+ if (fact !== undefined && !/^(?:[A-Z]{3,4}|_[A-Z]{2,6})$/.test(fact))
283
+ throw new UsageError(`--fact is a GEDCOM tag (BIRT, DEAT, NAME, SEX…), not "${opts.fact}"`);
284
+ const x = create(tree, "conflict", { title: args[0].trim(), ...(fact ? { fact } : {}), subject, claims, state: "open", note: opts.note }, (id) => `+${id} conflict "${truncate(args[0], 60)}"`, subject);
281
285
  return { text: written(tree), data: { conflict: x } };
282
286
  },
283
287
  }, {
@@ -10,6 +10,8 @@ import { mainPerson } from "../core/kin.js";
10
10
  import { resolvePerson } from "../core/people.js";
11
11
  import { knownGenerations, ownerName, pedigree, personCard, recent, treeStats } from "../core/overview.js";
12
12
  import { displayName } from "../core/people.js";
13
+ import { humanTask } from "../cli/human.js";
14
+ export { humanTask };
13
15
  import { lacksImages } from "../core/queue.js";
14
16
  import { rankedQueue, waitingForUser } from "./tasks.js";
15
17
  /** Most rows a section of `recent` lists; the rest is counted. */
@@ -64,23 +66,6 @@ function drawPedigree(root, lang) {
64
66
  walk(root, "");
65
67
  return out;
66
68
  }
67
- /** What the agent wrote into a task, for the user: the records it names by ID by their names ("S0001" → its title). */
68
- export function humanTask(tree, text, lang) {
69
- const name = (id) => {
70
- const r = tree.get(id);
71
- if (!r)
72
- return id;
73
- if (r.type === "session")
74
- return ui(lang, "ui.plan.session", { day: humanDay(r.started, lang) });
75
- if (r.type === "person")
76
- return `${displayName(r)} [${r.id}]`;
77
- if (r.type === "family")
78
- return r.partners.map((x) => tree.get(x)).filter((x) => !!x).map(displayName).join(" & ") || id;
79
- return r.title;
80
- };
81
- // A list of IDs as a list of names, each once (two sessions of one day read as one).
82
- return text.replace(/\b[PFSBN]\d{4,}\b(?:, [PFSBN]\d{4,}\b)*/g, (ids) => [...new Set(ids.split(", ").map(name))].join(", "));
83
- }
84
69
  function more(n, lang) {
85
70
  return n > 0 ? ` ${ui(lang, "ui.recent.more", { n })}` : undefined;
86
71
  }
@@ -145,13 +145,18 @@ THE STROM APP'S EDITS, ANOTHER FAMILY TREE (the user changed the tree in the Str
145
145
  --only 1,3). Additions become leads; a change to a fact a record proves becomes a conflict (sync.edits: the
146
146
  user's choice). A file of another research, or of few of our people, is refused; strom sync undo I… takes a
147
147
  sync back. After it: the user opens the research in the Strom app again to have the tree updated.
148
+ Straight from the app: strom sync --app (the user presses Send in the app). With strom-research:// links set
149
+ up on this computer (the user's yes: strom link on, the setup wizard; strom link status says) the app starts it
150
+ itself with one click, and opens an excerpt of an entry in full quality; its menus also open the research, a
151
+ conversation with you (of one person), a task that waits for the user, a person's review, a new direction — each
152
+ said and asked in a terminal first. Tell the user once when they use the app.
148
153
  STORIES OF THE ANCESTORS (the setting stories — on by default; strom shows it)
149
154
  Once records tell a person's life (a baptism and more facts from records), strom proposes a narrate
150
155
  task: write the story for the family book — plain words, the research language, every statement on a
151
156
  recorded fact (strom story set P… --text @notes/story-P….md --fact E… …). More facts later: it
152
157
  proposes adding to it. Stories wait behind all the research; working alone
153
158
  (strom run), every few sessions one comes first. In a conversation the user leads: write one when they
154
- want it, or offer it when the research has a pause. A story is a draft until the user approves it (--final). When strom says to tell
159
+ want it, or offer it when the research has a pause. A story is a draft until the user approves it (strom story approve P…; the Strom app asks it too). When strom says to tell
155
160
  the user (once, not in every conversation): in a sentence, and that they may say no (strom config set
156
161
  stories no).
157
162
 
@@ -21,5 +21,6 @@ import "./output.js";
21
21
  import "./session.js";
22
22
  import "./gates.js";
23
23
  import "./hooks.js";
24
+ import "./links.js";
24
25
  import "./checks.js";
25
26
  import "./setup.js";