@tiagojct/subsub 0.17.0 → 0.19.0
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 +25 -0
- package/README.md +4 -3
- package/THIRD_PARTY_NOTICES +98 -96
- package/dist/config.js +1 -1
- package/dist/feedback.js +55 -0
- package/dist/notes.js +41 -11
- package/dist/read.js +2 -1
- package/dist/subsub.js +80 -6
- package/dist/web.js +22 -5
- package/package.json +1 -1
- package/prompts/compare.md +1 -1
- package/prompts/gaps.md +1 -1
- package/prompts/lit.md +26 -7
- package/roles/researcher.md +5 -2
- package/src/config.ts +1 -1
- package/src/feedback.ts +70 -0
- package/src/notes.ts +44 -10
- package/src/read.ts +2 -1
- package/src/subsub.ts +83 -9
- package/src/web.ts +20 -5
- package/templates/Zotero agent.md +1 -1
- package/web/app.css +211 -168
- package/web/app.js +100 -6
- package/web/fonts/AtkinsonHyperlegibleMono.woff2 +0 -0
- package/web/fonts/AtkinsonHyperlegibleNext.woff2 +0 -0
- package/web/fonts/Literata-Italic.woff2 +0 -0
- package/web/fonts/Literata.woff2 +0 -0
- package/web/i18n.js +10 -1
- package/web/icon.svg +4 -4
- package/web/index.html +2 -1
- package/web/fonts/IBMPlexMono.woff2 +0 -0
- package/web/fonts/IBMPlexSans.woff2 +0 -0
- package/web/fonts/IBMPlexSerif.woff2 +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
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.19.0 (2026-10-16)
|
|
6
|
+
|
|
7
|
+
A new look for the browser view, the website, the tutorial and the workshop, after the engraved plates of 19th-century natural history books.
|
|
8
|
+
|
|
9
|
+
- A grey-green paper, blue-black ink, and four colours with one meaning each: blue for the Researcher and for links, madder red for the Librarian, green for what is applied or checked, ochre for a preview. In dark mode the page is a blue-black plate.
|
|
10
|
+
- Type: Literata for text, with true italics; Atkinson Hyperlegible Next for the interface; Atkinson Hyperlegible Mono for commands. They replace IBM Plex.
|
|
11
|
+
- The notes pane shows what a note was written from: a line under `evidence:`, solid for the full text, dashed for the abstract, dotted for the metadata.
|
|
12
|
+
- Reading mode in the notes pane: larger type and more space between lines, for long notes. Sub-Sub remembers it on this computer. The tutorial has the same button.
|
|
13
|
+
- In a preview, a tag that would be removed is struck through instead of red. Errors show in ink, with a word, so that red belongs to the Librarian only. Each mode button keeps its own colour, selected or not.
|
|
14
|
+
- A preview has a dashed frame: nothing has changed yet.
|
|
15
|
+
|
|
16
|
+
## 0.18.0 (2026-10-15)
|
|
17
|
+
|
|
18
|
+
- Figures and tables from the PDF. With the Zotero server 0.6.0, Sub-Sub can cut a figure or a table out of an item's PDF (`zotero_get_figure`, with the label of its caption, such as "Figure 2" or "Table 1") and show it in a note, with its caption and a link to the page. `/read critique` shows the table or figure with the main result. The images are PNG files in `Literature/figures/`; the notes pane and Obsidian show them. When the figure cannot be cut out, Sub-Sub takes the whole page and says so.
|
|
19
|
+
- In a note written from the full text, the page after each quote, "(p. N)", becomes a link that opens the PDF in Zotero at the page where Sub-Sub found the quote. The label stays as written; the link goes to the right page even when the printed page number differs from the PDF's.
|
|
20
|
+
- `/feedback [what happened]` (Report a problem in the browser) writes a draft bug report in `Inbox/`: the versions, the mode and model, the output of `subsub doctor` and the last error, with your folders, email addresses and keys taken out. You read it and post it on GitHub; Sub-Sub sends nothing.
|
|
21
|
+
- Preprints (medRxiv, bioRxiv, Research Square, SSRN, arXiv) are marked as not peer reviewed wherever Sub-Sub uses them: "(preprint)" in evidence tables and comparison matrices, and in the text. When a journal version exists, Sub-Sub uses and cites it instead.
|
|
22
|
+
- `/lit` rates how hard the search is (1 to 10) and allows none, one or two follow-up search rounds to match, with one publication window for every search; it stops early when the sources cover the question. The note's Methods give the rating and the rounds. `/gaps` gives a ranked list of 5 to 15 works and does not read them. This keeps the cost of a search in proportion to the question.
|
|
23
|
+
- In the browser view, every table in a note or a reply has a Download CSV button: evidence tables, extraction sheets, comparison matrices. The file opens in Excel (with semicolons when the interface is in Portuguese, as Excel expects there), Rayyan or Covidence.
|
|
24
|
+
- A `/lit` note (and a reading list) shows the flow of works under Methods: found, unique, screened, read and used, with what was left out at each step. It is a Mermaid diagram, so Obsidian, Quarto and GitHub draw it; the browser view draws it too.
|
|
25
|
+
- The quote check is stricter. A quote may skip text only at a page break (the running footer and header); two passages of one page joined into one quote, without [...] for the omission, are refused. Zotero marks page breaks in the full text, so the check knows where they are.
|
|
26
|
+
- `/lit reading list:` no longer ends with "the answer in three lines": a reading list states no results, in the reply too.
|
|
27
|
+
- In Portuguese, the browser button for `/lit-note` is now Nota de literatura, so that it is not confused with the reading notes of `/read`.
|
|
28
|
+
- Delete in the browser view keeps the conversation's subfolder in `~/.subsub/trash`, so that moving it back is one step.
|
|
29
|
+
|
|
5
30
|
## 0.17.0 (2026-10-14)
|
|
6
31
|
|
|
7
32
|
For people who are learning to read the literature:
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sub-Sub
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@tiagojct/subsub)
|
|
4
4
|
[](https://subsub.tiagojacinto.eu)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
@@ -24,7 +24,7 @@ Each mode can have its own model. Switch with `/researcher` and `/librarian`, or
|
|
|
24
24
|
|
|
25
25
|
Every library change works the same way in both modes:
|
|
26
26
|
|
|
27
|
-
- The Zotero server computes the change first. Sub-Sub shows it to you (
|
|
27
|
+
- The Zotero server computes the change first. Sub-Sub shows it to you (the tags it adds and the tags it removes) and applies it only when you select Yes. The check is in the program, so the model cannot skip it.
|
|
28
28
|
- If you edit an item in Zotero while the preview is open, Sub-Sub shows you the new preview before it changes anything. Each change also sends the item's version, so a field you edit at the moment of writing is not overwritten.
|
|
29
29
|
- Every change is journaled, including new collections. `/undo` reverts the last one, and the browser view shows an Undo button under each change.
|
|
30
30
|
- Nothing is deleted: items go to the Zotero trash. Only tags from your tag list can be added.
|
|
@@ -95,6 +95,7 @@ Commands in Sub-Sub:
|
|
|
95
95
|
| `/researcher`, `/librarian` | Switch the mode |
|
|
96
96
|
| `/subsub` | Zotero connection, mode, model and library counts |
|
|
97
97
|
| `/history`, `/undo [id]` | Recent library changes; undo the last one or one by its id |
|
|
98
|
+
| `/feedback [what happened]` | A draft bug report in `Inbox/`, with versions, `subsub doctor` and the last error, and without your folders, emails or keys. You post it on GitHub; Sub-Sub sends nothing |
|
|
98
99
|
| `/ai-statement [uses]` | A draft statement on the use of AI for a manuscript, with the Sub-Sub version, its DOI and your models |
|
|
99
100
|
| `/tag-batch [size]` | Tag items that have no topic, with your tag list |
|
|
100
101
|
| `/clean-tags` | Find tags that are not in your tag list and propose fixes |
|
|
@@ -182,4 +183,4 @@ This DOI always points to the latest version; each version also has its own DOI
|
|
|
182
183
|
|
|
183
184
|
## Licence
|
|
184
185
|
|
|
185
|
-
[MIT](LICENSE), Tiago Jacinto. The `/lit`, `/compare` and `/review` prompts adapt text from [Feynman](https://github.com/Companion-Inc/feynman) (MIT); see [THIRD_PARTY_NOTICES](THIRD_PARTY_NOTICES).
|
|
186
|
+
[MIT](LICENSE), Tiago Jacinto. The `/lit`, `/compare` and `/review` prompts adapt text from [Feynman](https://github.com/Companion-Inc/feynman) (MIT); see [THIRD_PARTY_NOTICES](THIRD_PARTY_NOTICES). Literata and Atkinson Hyperlegible fonts: SIL Open Font License.
|
package/THIRD_PARTY_NOTICES
CHANGED
|
@@ -55,99 +55,101 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
55
55
|
SOFTWARE.
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
web/fonts
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Copyright
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
This
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
-----------------------------------------------------------
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
redistributed
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
Software,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
58
|
+
web/fonts/, the website, the tutorial and the workshop use Literata, Atkinson
|
|
59
|
+
Hyperlegible Next and Atkinson Hyperlegible Mono, subset to Latin. Their licence:
|
|
60
|
+
|
|
61
|
+
Copyright 2017 The Literata Project Authors (https://github.com/googlefonts/literata)
|
|
62
|
+
Copyright 2020-2024 The Atkinson Hyperlegible Next Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next)
|
|
63
|
+
Copyright 2020-2024 The Atkinson Hyperlegible Mono Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next-mono)
|
|
64
|
+
|
|
65
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
66
|
+
|
|
67
|
+
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
-----------------------------------------------------------
|
|
71
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
72
|
+
-----------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
PREAMBLE
|
|
75
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
76
|
+
development of collaborative font projects, to support the font creation
|
|
77
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
78
|
+
open framework in which fonts may be shared and improved in partnership
|
|
79
|
+
with others.
|
|
80
|
+
|
|
81
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
82
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
83
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
84
|
+
redistributed and/or sold with any software provided that any reserved
|
|
85
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
86
|
+
however, cannot be released under any other type of license. The
|
|
87
|
+
requirement for fonts to remain under this license does not apply
|
|
88
|
+
to any document created using the fonts or their derivatives.
|
|
89
|
+
|
|
90
|
+
DEFINITIONS
|
|
91
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
92
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
93
|
+
include source files, build scripts and documentation.
|
|
94
|
+
|
|
95
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
96
|
+
copyright statement(s).
|
|
97
|
+
|
|
98
|
+
"Original Version" refers to the collection of Font Software components as
|
|
99
|
+
distributed by the Copyright Holder(s).
|
|
100
|
+
|
|
101
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
102
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
103
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
104
|
+
new environment.
|
|
105
|
+
|
|
106
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
107
|
+
writer or other person who contributed to the Font Software.
|
|
108
|
+
|
|
109
|
+
PERMISSION & CONDITIONS
|
|
110
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
111
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
112
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
113
|
+
Software, subject to the following conditions:
|
|
114
|
+
|
|
115
|
+
1) Neither the Font Software nor any of its individual components,
|
|
116
|
+
in Original or Modified Versions, may be sold by itself.
|
|
117
|
+
|
|
118
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
119
|
+
redistributed and/or sold with any software, provided that each copy
|
|
120
|
+
contains the above copyright notice and this license. These can be
|
|
121
|
+
included either as stand-alone text files, human-readable headers or
|
|
122
|
+
in the appropriate machine-readable metadata fields within text or
|
|
123
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
124
|
+
|
|
125
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
126
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
127
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
128
|
+
presented to the users.
|
|
129
|
+
|
|
130
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
131
|
+
Software shall not be used to promote, endorse or advertise any
|
|
132
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
133
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
134
|
+
permission.
|
|
135
|
+
|
|
136
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
137
|
+
must be distributed entirely under this license, and must not be
|
|
138
|
+
distributed under any other license. The requirement for fonts to
|
|
139
|
+
remain under this license does not apply to any document created
|
|
140
|
+
using the Font Software.
|
|
141
|
+
|
|
142
|
+
TERMINATION
|
|
143
|
+
This license becomes null and void if any of the above conditions are
|
|
144
|
+
not met.
|
|
145
|
+
|
|
146
|
+
DISCLAIMER
|
|
147
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
148
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
149
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
150
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
151
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
152
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
153
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
154
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
155
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
package/dist/config.js
CHANGED
|
@@ -57,7 +57,7 @@ export function uiLanguage(language) {
|
|
|
57
57
|
return /portug|^pt\b|^pt-/i.test(language ?? "") ? "pt" : "en";
|
|
58
58
|
}
|
|
59
59
|
/** The Zotero server release that this Sub-Sub version runs when there is no local server folder. */
|
|
60
|
-
export const SERVER_VERSION = "0.
|
|
60
|
+
export const SERVER_VERSION = "0.6.0";
|
|
61
61
|
/** The Starbuck release (PyPI) that this Sub-Sub version runs when there is no local Starbuck folder. */
|
|
62
62
|
export const STARBUCK_VERSION = "0.2.0";
|
|
63
63
|
export const STARBUCK_SOURCE = `starbuck==${STARBUCK_VERSION}`;
|
package/dist/feedback.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /feedback: a draft bug report or idea in the Inbox, for the user to read and post on GitHub.
|
|
3
|
+
* Sub-Sub sends nothing: the user decides what to share. The home and Sub-Sub folders, email
|
|
4
|
+
* addresses and keys are taken out before the draft is written.
|
|
5
|
+
*/
|
|
6
|
+
export const ISSUES_URL = "https://github.com/tiagojct/subsub/issues/new";
|
|
7
|
+
/** Text made safe to post: the Sub-Sub and home folders, email addresses and keys taken out. */
|
|
8
|
+
export function redact(text, home, vault) {
|
|
9
|
+
let out = text;
|
|
10
|
+
if (vault)
|
|
11
|
+
out = out.split(vault).join("<Sub-Sub folder>");
|
|
12
|
+
if (home)
|
|
13
|
+
out = out.split(home).join("~");
|
|
14
|
+
return out
|
|
15
|
+
.replace(/[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g, "<email>")
|
|
16
|
+
.replace(/\b(?:sk-|AIza|ghp_|gho_|github_pat_|xox[bp]-)[\w-]{8,}/g, "<key>")
|
|
17
|
+
.replace(/\b[A-Za-z0-9_-]{32,}\b/g, "<key>");
|
|
18
|
+
}
|
|
19
|
+
export function feedbackNote(f) {
|
|
20
|
+
const clean = (s) => redact(s, f.home, f.vault);
|
|
21
|
+
return [
|
|
22
|
+
"---",
|
|
23
|
+
"type: feedback",
|
|
24
|
+
`created: ${f.date}`,
|
|
25
|
+
"---",
|
|
26
|
+
"",
|
|
27
|
+
"# Sub-Sub feedback",
|
|
28
|
+
"",
|
|
29
|
+
`Read this draft, take out anything you do not want to share, then post it at ${ISSUES_URL}. Sub-Sub has sent nothing.`,
|
|
30
|
+
"",
|
|
31
|
+
"## What happened",
|
|
32
|
+
"",
|
|
33
|
+
clean(f.what.trim()) || "(what you did, and what you saw)",
|
|
34
|
+
"",
|
|
35
|
+
"## What you expected",
|
|
36
|
+
"",
|
|
37
|
+
"(what should have happened)",
|
|
38
|
+
"",
|
|
39
|
+
"## Set-up",
|
|
40
|
+
"",
|
|
41
|
+
`- ${clean(f.versions)}`,
|
|
42
|
+
...f.setup.map((l) => `- ${clean(l)}`),
|
|
43
|
+
"",
|
|
44
|
+
"## subsub doctor",
|
|
45
|
+
"",
|
|
46
|
+
"```",
|
|
47
|
+
clean(f.checks.trim()),
|
|
48
|
+
"```",
|
|
49
|
+
"",
|
|
50
|
+
"## Last error",
|
|
51
|
+
"",
|
|
52
|
+
f.lastError ? ["```", clean(f.lastError.trim()).slice(0, 2000), "```"].join("\n") : "(none in this session)",
|
|
53
|
+
"",
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
package/dist/notes.js
CHANGED
|
@@ -77,12 +77,16 @@ export function matchable(s) {
|
|
|
77
77
|
.toLowerCase()
|
|
78
78
|
.replace(/[^\p{L}\p{N}]+/gu, "");
|
|
79
79
|
}
|
|
80
|
+
/** The full text as matchable pages joined by "|": Zotero marks a page break with a form feed. */
|
|
81
|
+
export function sourceOf(fullText) {
|
|
82
|
+
return fullText.split("\f").map(matchable).join("|");
|
|
83
|
+
}
|
|
80
84
|
/**
|
|
81
85
|
* The quotes that are not in the full text. A quote with an ellipsis is checked in parts;
|
|
82
86
|
* square brackets (an editor's insertion) are left out of the comparison.
|
|
83
87
|
*/
|
|
84
88
|
export function missingQuotes(note, fullText, already = []) {
|
|
85
|
-
const source =
|
|
89
|
+
const source = sourceOf(fullText);
|
|
86
90
|
const old = new Set(already);
|
|
87
91
|
return quotesIn(note)
|
|
88
92
|
.filter((q) => !old.has(q))
|
|
@@ -92,33 +96,59 @@ export function missingQuotes(note, fullText, already = []) {
|
|
|
92
96
|
});
|
|
93
97
|
}
|
|
94
98
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
99
|
+
* Where a passage starts in the source (from sourceOf), or -1. It must be there word for word.
|
|
100
|
+
* The only gap allowed is a page break with the running footer and header around it (groups of
|
|
101
|
+
* three words in order, at most `gap` characters apart), so that a quote cannot join two passages
|
|
102
|
+
* of one page without marking the omission.
|
|
97
103
|
*/
|
|
98
|
-
export function
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
export function locate(source, passage, gap = 300) {
|
|
105
|
+
const whole = source.indexOf(matchable(passage));
|
|
106
|
+
if (whole >= 0)
|
|
107
|
+
return whole;
|
|
101
108
|
const words = passage.split(/\s+/).map(matchable).filter(Boolean);
|
|
102
109
|
const groups = [];
|
|
103
110
|
for (let i = 0; i < words.length; i += 3)
|
|
104
111
|
groups.push(words.slice(i, i + 3).join(""));
|
|
105
112
|
if (groups.length < 2)
|
|
106
|
-
return
|
|
113
|
+
return -1;
|
|
107
114
|
for (let start = source.indexOf(groups[0]), tries = 0; start >= 0 && tries < 50; start = source.indexOf(groups[0], start + 1), tries++) {
|
|
108
115
|
let end = start + groups[0].length;
|
|
109
116
|
let ok = true;
|
|
110
117
|
for (const g of groups.slice(1)) {
|
|
111
118
|
const at = source.indexOf(g, end);
|
|
112
|
-
if (at < 0 || at - end > gap) {
|
|
119
|
+
if (at < 0 || (at > end && (at - end > gap || !source.slice(end, at).includes("|")))) {
|
|
113
120
|
ok = false;
|
|
114
121
|
break;
|
|
115
122
|
}
|
|
116
123
|
end = at + g.length;
|
|
117
124
|
}
|
|
118
125
|
if (ok)
|
|
119
|
-
return
|
|
126
|
+
return start;
|
|
120
127
|
}
|
|
121
|
-
return
|
|
128
|
+
return -1;
|
|
129
|
+
}
|
|
130
|
+
export function appearsIn(source, passage, gap = 300) {
|
|
131
|
+
return locate(source, passage, gap) >= 0;
|
|
132
|
+
}
|
|
133
|
+
/** The page of the PDF (1-based, as Zotero's reader counts) where a passage starts, or undefined. */
|
|
134
|
+
export function pageOf(source, passage) {
|
|
135
|
+
const at = locate(source, passage);
|
|
136
|
+
return at < 0 ? undefined : source.slice(0, at).split("|").length;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Each "(p. N)" after a quote becomes a link that opens the PDF in Zotero at the page where the quote
|
|
140
|
+
* is. The label stays as written (the page printed on the paper may differ from the PDF's count).
|
|
141
|
+
*/
|
|
142
|
+
export function withPageLinks(text, source, attachment) {
|
|
143
|
+
const link = (quote, label) => {
|
|
144
|
+
const page = pageOf(source, quote.split(/\s*(?:\.\.\.|…|\[\s*(?:\.\.\.|…)\s*\])\s*/)[0]);
|
|
145
|
+
return page ? `([${label}](zotero://open-pdf/library/items/${attachment}?page=${page}))` : `(${label})`;
|
|
146
|
+
};
|
|
147
|
+
const PAGE = String.raw `\((pp?\.\s?\d+(?:\s?[-–]\s?\d+)?)\)`;
|
|
148
|
+
return text
|
|
149
|
+
.replace(new RegExp(String.raw `(")([^"\n]{25,600})("\s*)${PAGE}`, "g"), (_m, o, q, c, label) => `${o}${q}${c}${link(q, label)}`)
|
|
150
|
+
.replace(new RegExp(String.raw `(“)([^“”\n]{25,600})(”\s*)${PAGE}`, "g"), (_m, o, q, c, label) => `${o}${q}${c}${link(q, label)}`)
|
|
151
|
+
.replace(new RegExp(String.raw `^(>[ \t]*)(.{25,}?)(\s*)${PAGE}[ \t]*$`, "gm"), (_m, o, q, c, label) => `${o}${q}${c}${link(q.replace(/^["“]|["”]$/g, ""), label)}`);
|
|
122
152
|
}
|
|
123
153
|
export function quoteProblem(missing) {
|
|
124
154
|
if (!missing.length)
|
|
@@ -127,6 +157,6 @@ export function quoteProblem(missing) {
|
|
|
127
157
|
return [
|
|
128
158
|
`${missing.length} quote(s) in this note are not in the item's full text:`,
|
|
129
159
|
...shown,
|
|
130
|
-
"Copy each quote word for word from the full text (zotero_get_fulltext)
|
|
160
|
+
"Copy each quote word for word from the full text (zotero_get_fulltext). Mark every omission with [...]: two passages joined without that mark are not a quote. Or remove the quote marks and say it in your own words. Then write the note again.",
|
|
131
161
|
].join("\n");
|
|
132
162
|
}
|
package/dist/read.js
CHANGED
|
@@ -59,6 +59,7 @@ function depth(r) {
|
|
|
59
59
|
"Depth: the whole paper, section by section. A critique needs the full text: if you can read only the abstract, say so, write what the abstract allows, and offer /request-copy to ask the authors for the paper.",
|
|
60
60
|
'Under "## Methods", "## Results" and "## Discussion": the passages that matter to judge the study, quoted with their place, each with one question for the user (for example: how were the participants selected? what was compared? what else could explain the result?).',
|
|
61
61
|
`Under "## Appraisal": name the design and the reporting guideline that applies (${GUIDELINES}), and tell the user to check the paper against it item by item. Do not copy the checklist. Then five to eight questions on the risk of bias for this design (selection, confounding, measurement, missing data, selective reporting), each with the passage to look at.`,
|
|
62
|
+
"If the zotero_get_figure tool is available, show the table or figure with the main result: call it with the label of its caption and put the Markdown line it returns under the passage that cites it, with the page link, and a question about what the user sees in it.",
|
|
62
63
|
];
|
|
63
64
|
}
|
|
64
65
|
}
|
|
@@ -79,7 +80,7 @@ export function readInstruction(r) {
|
|
|
79
80
|
"",
|
|
80
81
|
"Rules:",
|
|
81
82
|
"- State no results, effect sizes, summaries or conclusions, in the note or in the chat. Quote the paper and ask; the user writes the answers.",
|
|
82
|
-
"- Copy quotes word for word from what you read. After each quote give its place:
|
|
83
|
+
"- Copy quotes word for word from what you read, and mark every omission with [...]. After each quote give its place: (p. N) only with evidence: full text (Sub-Sub links it to that page of the PDF), otherwise the section (Abstract, Methods, Results).",
|
|
83
84
|
`- The note is Literature/<citekey>.md in the Sub-Sub folder, with the front matter of a literature note from the shared rules plus type: reading-note and purpose: ${r.purpose}; evidence says what you read. If the note exists, do not overwrite it: add a section "## Reading ${r.date}: ${r.purpose}" at the end with edit, with the content below one heading level lower.`,
|
|
84
85
|
'- Start with "## Why I am reading this" (the reason, in the user\'s words when they gave them) and end with one line "Next step:" (what to read or do next).',
|
|
85
86
|
"",
|
package/dist/subsub.js
CHANGED
|
@@ -14,14 +14,17 @@
|
|
|
14
14
|
* session_shutdown. Tools are registered once and use the current bridge.
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
17
18
|
import { dirname, join } from "node:path";
|
|
18
19
|
import { fileURLToPath } from "node:url";
|
|
19
20
|
import { Bridge } from "./bridge.js";
|
|
20
21
|
import { expand, findUv, FREE_MODELS, loadConfig, parseEnvFile, serverCommand, starbuckCommand, starbuckEnabled, uiLanguage, who, } from "./config.js";
|
|
21
22
|
import { coerceArgs, loosenArrays, resolvePathArgs } from "./args.js";
|
|
22
23
|
import { aiStatement } from "./statement.js";
|
|
24
|
+
import { feedbackNote, ISSUES_URL } from "./feedback.js";
|
|
25
|
+
import { formatChecks, runChecks } from "./doctor.js";
|
|
23
26
|
import { PURPOSES, readArgs, readInstruction } from "./read.js";
|
|
24
|
-
import { applyEdits, checkLiteratureNote, field, frontMatter, itemKeyOf, missingQuotes, quoteProblem, quotesIn } from "./notes.js";
|
|
27
|
+
import { applyEdits, checkLiteratureNote, field, frontMatter, itemKeyOf, missingQuotes, quoteProblem, quotesIn, sourceOf, withPageLinks } from "./notes.js";
|
|
25
28
|
import { buildPolicy, isSecret, judgePath, normalizeToolPath, realish, within } from "./paths.js";
|
|
26
29
|
import { describeArgs, formatPreview, isEmptyPreview } from "./preview.js";
|
|
27
30
|
import { perMinuteWait, providerTrouble, TROUBLE_TEXT, TROUBLE_TEXT_PT } from "./keys.js";
|
|
@@ -127,6 +130,8 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
127
130
|
/** True under `subsub web`: pi runs in RPC mode and the web view shows the state that the terminal header shows. */
|
|
128
131
|
const webView = process.env.SUBSUB_WEB === "1";
|
|
129
132
|
let lastError;
|
|
133
|
+
/** The last provider or tool error of the session, for /feedback. */
|
|
134
|
+
let recentError;
|
|
130
135
|
let resumes = 0;
|
|
131
136
|
let resumeTimer;
|
|
132
137
|
let turnStart = 0;
|
|
@@ -319,13 +324,19 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
319
324
|
const m = event.message;
|
|
320
325
|
if (m.role !== "assistant" || m.stopReason !== "error")
|
|
321
326
|
return;
|
|
322
|
-
lastError = m.errorMessage;
|
|
327
|
+
lastError = recentError = m.errorMessage;
|
|
323
328
|
if (webView || perMinuteWait(m.errorMessage) !== undefined)
|
|
324
329
|
return;
|
|
325
330
|
const kind = providerTrouble(m.errorMessage);
|
|
326
331
|
if (kind)
|
|
327
332
|
ctx.ui.notify(`Sub-Sub: ${trouble[kind]}`, "warning");
|
|
328
333
|
});
|
|
334
|
+
pi.on("tool_result", async (event) => {
|
|
335
|
+
if (!event.isError)
|
|
336
|
+
return;
|
|
337
|
+
const text = event.content.map((c) => (c.type === "text" ? c.text : "")).join(" ");
|
|
338
|
+
recentError = `${event.toolName ?? "tool"}: ${text}`.slice(0, 2000);
|
|
339
|
+
});
|
|
329
340
|
// A per-minute limit (free tiers): wait as long as the provider says, then continue the task,
|
|
330
341
|
// up to three times per request of the user. Anything the user types cancels the wait.
|
|
331
342
|
pi.on("agent_end", async (_event, ctx) => {
|
|
@@ -507,14 +518,28 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
507
518
|
return undefined;
|
|
508
519
|
const full = await fullTextOf(key, signal);
|
|
509
520
|
// Without the full text (no PDF indexed, Zotero closed) there is nothing to check against.
|
|
510
|
-
|
|
521
|
+
if (!full)
|
|
522
|
+
return undefined;
|
|
523
|
+
const missing = quoteProblem(missingQuotes(text, full.text, quotesIn(before)));
|
|
524
|
+
// A note that passes is written with its page references linked to the PDF.
|
|
525
|
+
if (!missing && full.attachment) {
|
|
526
|
+
const link = (s) => withPageLinks(s, sourceOf(full.text), full.attachment);
|
|
527
|
+
if (name === "write")
|
|
528
|
+
input.content = link(String(input.content ?? ""));
|
|
529
|
+
else
|
|
530
|
+
for (const e of input.edits)
|
|
531
|
+
if (typeof e.newText === "string")
|
|
532
|
+
e.newText = link(e.newText);
|
|
533
|
+
}
|
|
534
|
+
return missing;
|
|
511
535
|
}
|
|
512
|
-
/** The whole indexed full text of an item, read in parts, kept for the session. */
|
|
536
|
+
/** The whole indexed full text of an item and its attachment, read in parts, kept for the session. */
|
|
513
537
|
const fullTexts = new Map();
|
|
514
538
|
async function fullTextOf(key, signal) {
|
|
515
539
|
if (fullTexts.has(key))
|
|
516
540
|
return fullTexts.get(key) ?? undefined;
|
|
517
541
|
let text = "";
|
|
542
|
+
let attachment;
|
|
518
543
|
let offset = 0;
|
|
519
544
|
try {
|
|
520
545
|
for (let part = 0; offset !== null && part < 20; part++) {
|
|
@@ -523,14 +548,16 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
523
548
|
break;
|
|
524
549
|
const data = (res.data ?? {});
|
|
525
550
|
text += data.text ?? "";
|
|
551
|
+
attachment ??= data.attachment;
|
|
526
552
|
offset = data.next_offset ?? null;
|
|
527
553
|
}
|
|
528
554
|
}
|
|
529
555
|
catch {
|
|
530
556
|
/* no full text: the check is skipped */
|
|
531
557
|
}
|
|
532
|
-
|
|
533
|
-
|
|
558
|
+
const full = text ? { text, attachment } : null;
|
|
559
|
+
fullTexts.set(key, full);
|
|
560
|
+
return full ?? undefined;
|
|
534
561
|
}
|
|
535
562
|
/**
|
|
536
563
|
* Starbuck writes its reports into a folder: report_dir, or _starbuck next to the
|
|
@@ -728,6 +755,53 @@ export async function createSubsub(pi, deps = {}) {
|
|
|
728
755
|
pi.sendUserMessage(readInstruction(req), ctx.isIdle() ? undefined : { deliverAs: "followUp" });
|
|
729
756
|
},
|
|
730
757
|
});
|
|
758
|
+
pi.registerCommand("feedback", {
|
|
759
|
+
description: "Sub-Sub: a draft bug report or idea in Inbox/, to read and post on GitHub yourself; Sub-Sub sends nothing [what happened]",
|
|
760
|
+
handler: async (args, ctx) => {
|
|
761
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
762
|
+
let piVersion = "";
|
|
763
|
+
try {
|
|
764
|
+
piVersion = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "..", "package.json"), "utf8")).version;
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
/* not needed for the report */
|
|
768
|
+
}
|
|
769
|
+
let checks;
|
|
770
|
+
try {
|
|
771
|
+
checks = deps.doctor ? deps.doctor() : formatChecks(runChecks(cfg, process.env));
|
|
772
|
+
}
|
|
773
|
+
catch (err) {
|
|
774
|
+
checks = `subsub doctor failed: ${err.message}`;
|
|
775
|
+
}
|
|
776
|
+
const text = feedbackNote({
|
|
777
|
+
what: String(args ?? ""),
|
|
778
|
+
date,
|
|
779
|
+
versions: `Sub-Sub ${ownVersion()}${piVersion ? `, pi ${piVersion}` : ""}, Node ${process.version}, ${process.platform} ${process.arch}`,
|
|
780
|
+
setup: [
|
|
781
|
+
`Mode: ${mode}. Model: ${ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "none"}.`,
|
|
782
|
+
`Language: ${cfg.language}. Reference checks (Starbuck): ${starbuckEnabled(cfg) ? "on" : "off"}.`,
|
|
783
|
+
`Zotero: ${library.zotero}.${bridge && Object.keys(bridge.errors).length ? ` Servers not running: ${Object.keys(bridge.errors).join(", ")}.` : ""}`,
|
|
784
|
+
],
|
|
785
|
+
checks,
|
|
786
|
+
lastError: recentError,
|
|
787
|
+
home: homedir(),
|
|
788
|
+
vault: cfg.vault,
|
|
789
|
+
});
|
|
790
|
+
const dir = join(cfg.vault ?? ctx.cwd, "Inbox");
|
|
791
|
+
let file = join(dir, `${date} Sub-Sub feedback.md`);
|
|
792
|
+
for (let n = 2; existsSync(file); n++)
|
|
793
|
+
file = join(dir, `${date} Sub-Sub feedback-${n}.md`);
|
|
794
|
+
try {
|
|
795
|
+
mkdirSync(dir, { recursive: true });
|
|
796
|
+
writeFileSync(file, text);
|
|
797
|
+
}
|
|
798
|
+
catch (err) {
|
|
799
|
+
ctx.ui.notify(`${L("Could not save it", "Não foi possível guardar")}: ${err.message}`, "warning");
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
ctx.ui.notify(L(`Sub-Sub: a draft report is in ${file}. Read it, add what you expected, take out anything you do not want to share, and post it at ${ISSUES_URL}. Sub-Sub has sent nothing.`, `Sub-Sub: há um rascunho do relatório em ${file}. Leia-o, acrescente o que esperava, retire o que não quer partilhar e publique-o em ${ISSUES_URL}. O Sub-Sub não enviou nada.`), "info");
|
|
803
|
+
},
|
|
804
|
+
});
|
|
731
805
|
pi.registerCommand("history", {
|
|
732
806
|
description: "Sub-Sub: recent library changes",
|
|
733
807
|
handler: async (_args, ctx) => {
|