Skip to content

Commit 84a6779

Browse files
authored
Re-convert + frontmatter + export + concurrency + notifications + reset (#9)
Phase 5a client-side features. Highlights: - Right-click re-convert (replace) with strict visibility — only shown when selection contains a PDF with a matching .md sibling. Action filters to those items only; never silently creates fresh .md for items that didn't have one. - Dedupe in-flight conversions per attachment id so repeated clicks on the same PDF don't queue multiple server-side jobs. - Block concurrent convert batches (addon.data.batchInFlight). Menu rejects loudly, notifier defers silently with a single 'queued' toast per cycle. - YAML frontmatter (title, authors, year, doi, url, zotero_key, citation_key) optionally prepended to every .md output. Default ON. - Export-to-folder sink (exportFolderPath) alongside the Zotero attachment. Naming: {citationKey || zoteroKey}.md. - Parallel concurrency (maxConcurrency 1-8). DB writes serialised through a global lock so SQLite doesn't race. - Per-item progress in the toast (N/M counter + truncated filename). - Managed progress hides on Zotero blur and re-appears on focus with the current state. - OS notification when a batch finishes and Zotero isn't focused. - Reset-to-defaults button in prefs (confirm dialog, clears every pref). - Plain Error for abort markers instead of DOMException (safer in the Z9 sandbox). - HTTP error labels use response.statusText ('HTTP 504 Gateway Timeout'). - skipIfExists matches on filename so siblings under the same parent don't shadow each other. Removed: - The cancel button. docling-serve has no per-task cancel API (upstream issue docling-project/docling-serve#447) and doesn't detect client disconnects (#401). README 'Known limitations' explains. - asyncMaxWaitSec pref + UI. Same dishonest pattern — timing out client- side just abandons a still-running server task.
1 parent 448a2eb commit 84a6779

20 files changed

Lines changed: 1116 additions & 117 deletions

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,37 @@ sections of the prefs UI itself. Hover any field for inline help.
9898

9999
---
100100

101+
## Known limitations
102+
103+
### No cancel or async timeout for an in-flight conversion
104+
105+
The plugin does not expose a cancel button, and the async-transport poll
106+
loop has no client-side timeout. This is intentional — docling-serve
107+
(the upstream server we talk to) currently has **no per-task cancel API**
108+
and **does not detect client disconnects** during processing, so any
109+
"give up" mechanism on the client side just orphans a server task that
110+
keeps running invisibly. Verified on docling-serve 1.18.0:
111+
112+
- The OpenAPI surface exposes no `DELETE` or `cancel` route.
113+
- The server source contains `# TODO: abort task!` markers but no
114+
implementation.
115+
- Tracking upstream:
116+
[docling-project/docling-serve#447 — Cancellation api](https://github.com/docling-project/docling-serve/issues/447)
117+
and
118+
[#401 — Interrupt Parsing on Disconnected Request](https://github.com/docling-project/docling-serve/issues/401).
119+
120+
Practical consequences:
121+
122+
- Closing the toast or quitting Zotero does **not** stop the server-side
123+
conversion. The PDF will continue to be processed in the background until
124+
it completes naturally.
125+
- If you start a long VLM batch and want it to stop, the only way is to
126+
restart `docling-serve`.
127+
- Once the upstream cancel API exists, we'll add a cancel button that
128+
actually does what it says.
129+
130+
---
131+
101132
## Acknowledgments
102133

103134
- The **[Docling team](https://github.com/docling-project)** for shipping

addon/content/preferences.xhtml

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,61 @@
4848
preference="skipIfExists"
4949
data-l10n-id="pref-skip-if-exists"
5050
/>
51+
<hbox align="center">
52+
<html:label
53+
for="zotero-docling-max-concurrency"
54+
data-l10n-id="pref-max-concurrency"
55+
style="min-width: 180px"
56+
></html:label>
57+
<html:input
58+
type="number"
59+
id="zotero-docling-max-concurrency"
60+
preference="maxConcurrency"
61+
min="1"
62+
max="8"
63+
step="1"
64+
style="width: 6em"
65+
></html:input>
66+
</hbox>
67+
<description data-l10n-id="pref-max-concurrency-help"></description>
68+
<checkbox
69+
id="zotero-docling-notify"
70+
preference="notifyOnComplete"
71+
data-l10n-id="pref-notify-on-complete"
72+
/>
73+
</groupbox>
74+
75+
<!-- ============================================================ -->
76+
<!-- Output (Phase 5a) -->
77+
<!-- ============================================================ -->
78+
<groupbox>
79+
<label><html:h2 data-l10n-id="pref-output-title"></html:h2></label>
80+
<checkbox
81+
id="zotero-docling-attach-to-item"
82+
preference="attachToItem"
83+
data-l10n-id="pref-attach-to-item"
84+
/>
85+
<checkbox
86+
id="zotero-docling-add-frontmatter"
87+
preference="addFrontmatter"
88+
data-l10n-id="pref-add-frontmatter"
89+
/>
90+
<description data-l10n-id="pref-frontmatter-help"></description>
91+
<hbox align="center">
92+
<html:label
93+
for="zotero-docling-export-folder"
94+
data-l10n-id="pref-export-folder"
95+
style="min-width: 180px"
96+
></html:label>
97+
<html:input
98+
type="text"
99+
id="zotero-docling-export-folder"
100+
preference="exportFolderPath"
101+
placeholder="/absolute/path/to/markdown/folder"
102+
style="flex: 1"
103+
></html:input>
104+
</hbox>
105+
<description data-l10n-id="pref-export-folder-help"></description>
51106
</groupbox>
52107

53108
<!-- ============================================================ -->
@@ -222,21 +277,6 @@
222277
style="width: 6em"
223278
></html:input>
224279
</hbox>
225-
<hbox align="center">
226-
<html:label
227-
for="zotero-docling-async-max-wait"
228-
data-l10n-id="pref-async-max-wait"
229-
style="min-width: 180px"
230-
></html:label>
231-
<html:input
232-
type="number"
233-
id="zotero-docling-async-max-wait"
234-
preference="asyncMaxWaitSec"
235-
min="0"
236-
step="60"
237-
style="width: 6em"
238-
></html:input>
239-
</hbox>
240280
</groupbox>
241281

242282
<!-- ============================================================ -->
@@ -254,6 +294,15 @@
254294
></html:textarea>
255295
</groupbox>
256296

297+
<!-- Reset to defaults — clears every plugin pref including serverUrl -->
298+
<hbox align="center">
299+
<button id="zotero-docling-reset" data-l10n-id="pref-reset"></button>
300+
<html:label
301+
data-l10n-id="pref-reset-help"
302+
style="margin-left: 1em; font-style: italic; opacity: 0.7"
303+
></html:label>
304+
</hbox>
305+
257306
<!-- Build info -->
258307
<vbox>
259308
<html:label

addon/locale/en-US/addon.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
menuitem-convert = Convert with Docling
2+
menuitem-reconvert = Re-convert with Docling (replace)
23
pref-pane-label = Zotero Docling

addon/locale/en-US/preferences.ftl

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ pref-auto-convert =
77
.label = Auto-convert new PDF attachments on import
88
pref-skip-if-exists =
99
.label = Skip items that already have a Markdown attachment
10+
pref-max-concurrency = Parallel conversions
11+
pref-max-concurrency-help = How many PDFs to convert in parallel within one batch. Default 1 (sequential, safest). Higher values only speed things up when paired with the async endpoint AND a docling-serve started with --workers ≥ 2.
12+
pref-notify-on-complete =
13+
.label = OS notification when a batch finishes (only if Zotero isn't focused)
14+
15+
pref-output-title = Output
16+
pref-attach-to-item =
17+
.label = Attach the .md as a Zotero child attachment (recommended)
18+
pref-add-frontmatter =
19+
.label = Prepend YAML frontmatter with Zotero metadata
20+
pref-frontmatter-help = Adds title, authors, year, doi, url, zotero_key, citation_key to every .md output as a YAML --- block at the top.
21+
pref-export-folder = Export folder
22+
pref-export-folder-help = Absolute path. If set, every converted .md is ALSO written here as "{citationKey || zoteroKey}.md". Leave empty to disable.
1023
1124
pref-conversion-title = Conversion
1225
pref-pipeline = Pipeline
@@ -45,9 +58,14 @@ pref-async-help = Submit the job to docling-serve's async endpoint and poll for
4558
pref-use-async =
4659
.label = Use the async endpoint (/v1/convert/file/async)
4760
pref-async-poll = Poll interval (s)
48-
pref-async-max-wait = Max wait (s, 0 = ∞)
4961
5062
pref-advanced-title = Advanced
5163
pref-advanced-help = JSON object whose keys are sent as additional form fields to docling-serve. Overrides anything above. Leave empty to disable.
5264
65+
pref-reset = Reset to defaults
66+
pref-reset-help = Reverts every Zotero Docling preference (including Server URL) to its built-in default.
67+
pref-reset-confirm-title = Reset Zotero Docling preferences?
68+
pref-reset-confirm-body = This reverts every plugin preference (Server URL, auto-convert, pipeline, VLM preset, output, etc.) to its built-in default. Your Zotero library and existing markdown attachments are not touched.
69+
pref-reset-done = Preferences reset to defaults
70+
5371
pref-build-info = { $name } { $version } · built { $time }

addon/prefs.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,20 @@ pref("advancedJson", "");
2929
// for short conversions. Turn on for long VLM jobs that would otherwise 504.
3030
pref("useAsyncEndpoint", false);
3131
pref("asyncPollIntervalSec", 5); // poll cadence in seconds (min 1)
32-
pref("asyncMaxWaitSec", 1800); // 30 min hard cap; 0 = wait indefinitely
32+
33+
// Phase 5a: client-side polish
34+
// Concurrent conversions in a batch (1 = sequential, current behavior).
35+
// More only helps when paired with the async endpoint AND a server started
36+
// with --workers >= 2; otherwise requests just queue server-side.
37+
pref("maxConcurrency", 1);
38+
// Prepend YAML frontmatter (title/authors/year/doi/url/zotero_key/citation_key)
39+
// to every .md output. Off-by-default would be a regression — most lit-lake
40+
// downstream tools (Obsidian, RAG pipelines) expect this metadata.
41+
pref("addFrontmatter", true);
42+
// Output sinks. Default: attach to the Zotero item (existing behavior).
43+
// Setting exportFolderPath to an absolute directory ALSO writes the .md
44+
// there, named "{citationKey || zoteroKey}.md". Both can be on at once.
45+
pref("attachToItem", true);
46+
pref("exportFolderPath", "");
47+
// OS-level notification when a batch finishes, only if Zotero isn't focused.
48+
pref("notifyOnComplete", false);

src/addon.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ class Addon {
2020
rows: Array<{ [dataKey: string]: string }>;
2121
};
2222
dialog?: DialogHelper;
23+
// True while a convert batch (menu-initiated or notifier-initiated) is
24+
// running. Used to soft-block a second batch from starting before the
25+
// first finishes — prevents progress-window stomping and ambiguous
26+
// status when prefs change mid-flight.
27+
batchInFlight?: boolean;
2328
};
2429
// Lifecycle hooks
2530
public hooks: typeof hooks;

src/hooks.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { initLocale, getString } from "./utils/locale";
22
import { registerMenu, unregisterMenu } from "./modules/menu";
33
import { registerNotifier, unregisterNotifier } from "./modules/notifier";
44
import { registerPrefsScripts } from "./modules/preferenceScript";
5+
import { onZoteroBlur, onZoteroFocus } from "./modules/ui";
56
import { createZToolkit } from "./utils/ztoolkit";
67

78
async function onStartup(): Promise<void> {
@@ -23,11 +24,23 @@ async function onStartup(): Promise<void> {
2324
addon.data.initialized = true;
2425
}
2526

26-
async function onMainWindowLoad(_win: _ZoteroTypes.MainWindow): Promise<void> {
27+
async function onMainWindowLoad(win: _ZoteroTypes.MainWindow): Promise<void> {
2728
// Fresh ztoolkit per window — the toolkit owns DOM lifetime.
2829
addon.data.ztoolkit = createZToolkit();
2930
// (Template had insertFTLIfNeeded("...-mainWindow.ftl") here; we don't ship
3031
// a mainWindow.ftl, so omitting it avoids "Missing resource" log spam.)
32+
33+
// Blur/focus listeners drive the managed-progress hide-on-blur behaviour
34+
// (the "stop showing the toast when user switches apps" UX). Re-show on
35+
// focus brings the latest state back.
36+
try {
37+
win.addEventListener("blur", () => onZoteroBlur());
38+
win.addEventListener("focus", () => onZoteroFocus());
39+
} catch (e) {
40+
Zotero.debug(
41+
`[zotero-docling] focus listeners failed (non-fatal): ${(e as Error).message}`,
42+
);
43+
}
3144
}
3245

3346
/**

0 commit comments

Comments
 (0)