@tiagojct/subsub 0.12.1 → 0.13.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.md CHANGED
@@ -1,8 +1,14 @@
1
1
  # Changes
2
2
 
3
- Sub-Sub follows the version numbers of npm: a change in the second number (0.12) can change how you use it; a change in the third (0.12.1) only fixes. Until the pilot results are in, new versions are fixes only, apart from changes that the pilot asks for.
3
+ Sub-Sub follows the version numbers of npm: a change in the second number (0.13) adds features or changes how you use it; a change in the third (0.13.1) only fixes.
4
4
 
5
- ## 0.12.1 (2026-10-07)
5
+ ## 0.13.1 (2026-10-07)
6
+
7
+ - A change that would change nothing (for example, citekeys for items that already have one) is no longer offered for approval. Sub-Sub says why instead.
8
+
9
+ ## 0.13.0 (2026-10-07)
10
+
11
+ The same changes were published first as 0.12.1.
6
12
 
7
13
  Files and notes without a parent item (Zotero server 0.5.5):
8
14
 
package/dist/preview.js CHANGED
@@ -196,6 +196,23 @@ export function formatPreview(tool, preview, lang = "en") {
196
196
  lines.push(s(p, 1500));
197
197
  return lines.join("\n");
198
198
  }
199
+ /**
200
+ * True when a preview has nothing to apply: every count it gives is zero and it creates,
201
+ * writes or undoes nothing. Such a change is not offered for approval.
202
+ */
203
+ export function isEmptyPreview(preview) {
204
+ if (!preview || typeof preview !== "object")
205
+ return false;
206
+ const p = preview;
207
+ const counts = ["would_import", "would_attach", "would_set_parent"].filter((k) => Array.isArray(p[k]));
208
+ if (typeof p.would_change !== "number" && !counts.length)
209
+ return false;
210
+ if (typeof p.would_change === "number" && p.would_change > 0)
211
+ return false;
212
+ if (counts.some((k) => p[k].length > 0))
213
+ return false;
214
+ return !["would_create", "undoing", "summary", "note_preview"].some((k) => p[k]);
215
+ }
199
216
  export function describeArgs(input) {
200
217
  return Object.entries(input)
201
218
  .map(([k, v]) => `${k}: ${s(v, 120)}`)
package/dist/subsub.js CHANGED
@@ -24,7 +24,7 @@ import { profileList, profileSpec, promptBlocked } from "./profiles.js";
24
24
  import { coerceArgs, loosenArrays, resolvePathArgs } from "./args.js";
25
25
  import { applyEdits, checkLiteratureNote } from "./notes.js";
26
26
  import { buildPolicy, isSecret, judgePath, normalizeToolPath, realish, within } from "./paths.js";
27
- import { describeArgs, formatPreview } from "./preview.js";
27
+ import { describeArgs, formatPreview, isEmptyPreview } from "./preview.js";
28
28
  import { perMinuteWait, providerTrouble, TROUBLE_TEXT, TROUBLE_TEXT_PT } from "./keys.js";
29
29
  import { headerLines, paintPreview, QUOTES, schemeFromAnsi, themeName } from "./look.js";
30
30
  import { systemAddition } from "./prompt.js";
@@ -442,6 +442,12 @@ export async function createSubsub(pi, deps = {}) {
442
442
  }
443
443
  if (preview.isError)
444
444
  return { block: true, reason: preview.text };
445
+ if (isEmptyPreview(preview.data)) {
446
+ return {
447
+ block: true,
448
+ reason: `Nothing to apply: the preview changes no item, so ${user} was not asked. Tell ${user} why in plain words; do not call ${name} again with the same input. Preview: ${JSON.stringify(preview.data).slice(0, 2000)}`,
449
+ };
450
+ }
445
451
  const title = L(`Sub-Sub: apply ${name.replace(/^(zotero|scholar)_/, "")}?`, `Sub-Sub: aplicar ${name.replace(/^(zotero|scholar)_/, "")}?`);
446
452
  let shown = formatPreview(name, preview.data ?? preview.text, lang);
447
453
  // The dialog may stay open for minutes. Before applying, compute the preview again: if the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiagojct/subsub",
3
- "version": "0.12.1",
3
+ "version": "0.13.1",
4
4
  "description": "Sub-Sub: a research assistant that works from your own Zotero library, in the browser or the terminal, built on pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/preview.ts CHANGED
@@ -207,6 +207,20 @@ export function formatPreview(tool: string, preview: unknown, lang: Lang = "en")
207
207
  return lines.join("\n");
208
208
  }
209
209
 
210
+ /**
211
+ * True when a preview has nothing to apply: every count it gives is zero and it creates,
212
+ * writes or undoes nothing. Such a change is not offered for approval.
213
+ */
214
+ export function isEmptyPreview(preview: unknown): boolean {
215
+ if (!preview || typeof preview !== "object") return false;
216
+ const p = preview as Obj;
217
+ const counts = ["would_import", "would_attach", "would_set_parent"].filter((k) => Array.isArray(p[k]));
218
+ if (typeof p.would_change !== "number" && !counts.length) return false;
219
+ if (typeof p.would_change === "number" && p.would_change > 0) return false;
220
+ if (counts.some((k) => (p[k] as unknown[]).length > 0)) return false;
221
+ return !["would_create", "undoing", "summary", "note_preview"].some((k) => p[k]);
222
+ }
223
+
210
224
  export function describeArgs(input: Record<string, unknown>): string {
211
225
  return Object.entries(input)
212
226
  .map(([k, v]) => `${k}: ${s(v, 120)}`)
package/src/subsub.ts CHANGED
@@ -41,7 +41,7 @@ import { profileList, profileSpec, promptBlocked } from "./profiles.ts";
41
41
  import { coerceArgs, loosenArrays, resolvePathArgs } from "./args.ts";
42
42
  import { applyEdits, checkLiteratureNote } from "./notes.ts";
43
43
  import { buildPolicy, isSecret, judgePath, normalizeToolPath, realish, within } from "./paths.ts";
44
- import { describeArgs, formatPreview } from "./preview.ts";
44
+ import { describeArgs, formatPreview, isEmptyPreview } from "./preview.ts";
45
45
  import { perMinuteWait, providerTrouble, TROUBLE_TEXT, TROUBLE_TEXT_PT } from "./keys.ts";
46
46
  import { headerLines, type LibraryState, paintPreview, QUOTES, type Scheme, schemeFromAnsi, themeName } from "./look.ts";
47
47
  import { systemAddition } from "./prompt.ts";
@@ -459,6 +459,12 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
459
459
  return { block: true, reason: `Preview failed: ${(err as Error).message}` };
460
460
  }
461
461
  if (preview.isError) return { block: true, reason: preview.text };
462
+ if (isEmptyPreview(preview.data)) {
463
+ return {
464
+ block: true,
465
+ reason: `Nothing to apply: the preview changes no item, so ${user} was not asked. Tell ${user} why in plain words; do not call ${name} again with the same input. Preview: ${JSON.stringify(preview.data).slice(0, 2000)}`,
466
+ };
467
+ }
462
468
  const title = L(`Sub-Sub: apply ${name.replace(/^(zotero|scholar)_/, "")}?`, `Sub-Sub: aplicar ${name.replace(/^(zotero|scholar)_/, "")}?`);
463
469
  let shown = formatPreview(name, preview.data ?? preview.text, lang);
464
470
  // The dialog may stay open for minutes. Before applying, compute the preview again: if the