@tiagojct/subsub 0.9.1 → 0.11.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/README.md +15 -16
- package/dist/bench.js +9 -7
- package/dist/bridge.js +2 -2
- package/dist/config.js +21 -8
- package/dist/doctor.js +2 -2
- package/dist/init.js +17 -10
- package/dist/look.js +3 -3
- package/dist/profiles.js +6 -6
- package/dist/prompt.js +2 -2
- package/dist/roles.js +25 -16
- package/dist/subsub.js +16 -13
- package/dist/web.js +4 -5
- package/docs/Subsub.md +18 -18
- package/package.json +1 -1
- package/prompts/alert.md +1 -1
- package/prompts/clean-tags.md +1 -1
- package/prompts/compare.md +1 -1
- package/prompts/digest.md +2 -2
- package/prompts/gaps.md +2 -2
- package/prompts/import-queue.md +1 -1
- package/prompts/lit-note.md +1 -1
- package/prompts/lit.md +2 -2
- package/prompts/manuscript.md +1 -1
- package/prompts/request-copy.md +1 -1
- package/prompts/review.md +1 -1
- package/prompts/synthesis.md +1 -1
- package/prompts/tag-batch.md +1 -1
- package/prompts/verify.md +1 -1
- package/roles/librarian.md +1 -1
- package/roles/researcher.md +49 -5
- package/src/bench.ts +9 -7
- package/src/bridge.ts +2 -2
- package/src/config.ts +25 -12
- package/src/doctor.ts +2 -2
- package/src/init.ts +19 -9
- package/src/look.ts +4 -5
- package/src/profiles.ts +6 -6
- package/src/prompt.ts +2 -2
- package/src/roles.ts +35 -18
- package/src/subsub.ts +18 -14
- package/src/web.ts +4 -5
- package/templates/Zotero agent.md +6 -6
- package/web/app.css +3 -2
- package/web/app.js +25 -13
- package/web/i18n.js +39 -29
- package/web/index.html +1 -1
package/src/look.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Only used when Sub-Sub runs as its own command in the terminal UI.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Mode } from "./config.ts";
|
|
8
7
|
|
|
9
8
|
export const BANNER = [
|
|
10
9
|
"╔═╗╦ ╦╔╗ ╔═╗╦ ╦╔╗",
|
|
@@ -25,7 +24,8 @@ export const QUOTES: Array<[string, string]> = [
|
|
|
25
24
|
["Call me Ishmael.", "Moby-Dick, ch. 1"],
|
|
26
25
|
];
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
/** The terminal theme, without -dark/-light. subsub-try-works is the other one shipped. */
|
|
28
|
+
export const DEFAULT_THEME = "subsub-glauca";
|
|
29
29
|
|
|
30
30
|
export type Scheme = "dark" | "light";
|
|
31
31
|
|
|
@@ -47,8 +47,7 @@ export function schemeFromAnsi(fgAnsi: string, themeName?: string): Scheme {
|
|
|
47
47
|
return "dark";
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
export function themeName(
|
|
51
|
-
const base = map[mode];
|
|
50
|
+
export function themeName(scheme: Scheme, base: string | undefined = DEFAULT_THEME): string | undefined {
|
|
52
51
|
return base ? `${base}-${scheme}` : undefined;
|
|
53
52
|
}
|
|
54
53
|
|
|
@@ -84,7 +83,7 @@ export function libraryLine(st: LibraryState): { text: string; warn: boolean } {
|
|
|
84
83
|
/** Header lines. Each line is cut to the width before it is coloured. */
|
|
85
84
|
export function headerLines(opts: {
|
|
86
85
|
version: string;
|
|
87
|
-
mode:
|
|
86
|
+
mode: "librarian" | "researcher";
|
|
88
87
|
/** Profile label, e.g. "Scholar". */
|
|
89
88
|
profile?: string;
|
|
90
89
|
model: string;
|
package/src/profiles.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* - reader: explains each step; reading notes are quotes with page numbers and
|
|
7
7
|
* questions; no summaries or syntheses; no bulk library changes.
|
|
8
|
-
* - scholar: literature notes, syntheses, searches, alerts;
|
|
8
|
+
* - scholar: literature notes, syntheses, searches, alerts; all library changes.
|
|
9
9
|
* - author: scholar plus manuscript work: citation check, bibliography, comments
|
|
10
10
|
* on the argument. Does not write the manuscript's paragraphs.
|
|
11
11
|
* - editor: everything, including drafting text for a manuscript on request.
|
|
@@ -46,13 +46,13 @@ export const PROFILE_SPECS: Record<Profile, ProfileSpec> = {
|
|
|
46
46
|
"- A literature note is a reading note: the full reference, then direct quotes with page numbers (or section names), and under each quote one question for {{user}} to answer. Do not write summaries, syntheses, conclusions or paragraphs that {{user}} could hand in as their own text. When a prompt or the shared rules ask for a literature note or a synthesis, make a reading note instead and say so.",
|
|
47
47
|
"- For searches, list works with one line each on what they study, not on what they conclude.",
|
|
48
48
|
"- If {{user}} asks for a summary, a synthesis or text for an assignment, say that the Reader profile does not write these, and offer quotes and questions. {{User}} can change the profile with /profile.",
|
|
49
|
-
"-
|
|
49
|
+
"- Tag review notes: at most 10 items. Explain each proposed tag in the Reason column.",
|
|
50
50
|
].join("\n"),
|
|
51
51
|
},
|
|
52
52
|
scholar: {
|
|
53
53
|
name: "scholar",
|
|
54
54
|
label: "Scholar",
|
|
55
|
-
summary: "literature notes, syntheses, searches and alerts;
|
|
55
|
+
summary: "literature notes, syntheses, searches and alerts; all library changes",
|
|
56
56
|
without: MANUSCRIPT,
|
|
57
57
|
withoutPrompts: ["review"],
|
|
58
58
|
batch: 25,
|
|
@@ -60,7 +60,7 @@ export const PROFILE_SPECS: Record<Profile, ProfileSpec> = {
|
|
|
60
60
|
"Profile: Scholar.",
|
|
61
61
|
"- Write literature notes and syntheses from what you read, with a source for every claim.",
|
|
62
62
|
"- Do not draft text for {{user}}'s manuscripts, theses or assignments. If asked, say that the Author and Editor profiles help with manuscripts (/profile).",
|
|
63
|
-
"-
|
|
63
|
+
"- Tag review notes: 25 items.",
|
|
64
64
|
].join("\n"),
|
|
65
65
|
},
|
|
66
66
|
author: {
|
|
@@ -75,7 +75,7 @@ export const PROFILE_SPECS: Record<Profile, ProfileSpec> = {
|
|
|
75
75
|
"- Write literature notes and syntheses from what you read, with a source for every claim.",
|
|
76
76
|
"- Help with {{user}}'s manuscripts: check citations against the library, export the bibliography, and comment on the argument, the structure and the evidence that is missing, with the sources that could support each point.",
|
|
77
77
|
"- Do not write the manuscript's paragraphs. Say what is missing and where the support is; {{user}} writes the text. The Editor profile drafts text (/profile).",
|
|
78
|
-
"-
|
|
78
|
+
"- Tag review notes: 25 items.",
|
|
79
79
|
].join("\n"),
|
|
80
80
|
},
|
|
81
81
|
editor: {
|
|
@@ -89,7 +89,7 @@ export const PROFILE_SPECS: Record<Profile, ProfileSpec> = {
|
|
|
89
89
|
"Profile: Editor. All tools.",
|
|
90
90
|
"- Write literature notes and syntheses from what you read, with a source for every claim.",
|
|
91
91
|
"- When {{user}} asks, draft text for a manuscript: cite library items as [@citekey], use only claims from sources you read, and mark drafted text clearly so that {{user}} can check it.",
|
|
92
|
-
"-
|
|
92
|
+
"- Tag review notes: 25 items unless {{user}} asks for more.",
|
|
93
93
|
].join("\n"),
|
|
94
94
|
},
|
|
95
95
|
};
|
package/src/prompt.ts
CHANGED
|
@@ -53,13 +53,13 @@ export function fill(text: string, cfg: Pick<SubsubConfig, "userName" | "about"
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export function roleText(mode: Mode, cfg?: Pick<SubsubConfig, "userName" | "about" | "profile">): string {
|
|
56
|
-
const raw = readIf(join(ROLE_DIR, `${mode}.md`)) ?? `You are the ${mode}.`;
|
|
56
|
+
const raw = readIf(join(ROLE_DIR, `${mode}.md`)) ?? `You are Sub-Sub, the ${mode}.`;
|
|
57
57
|
return cfg ? fill(raw, cfg) : raw;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export function systemAddition(mode: Mode, cfg: SubsubConfig, cwd: string): string {
|
|
61
61
|
const user = who(cfg);
|
|
62
|
-
const parts = [`# Sub-Sub: ${mode} mode`, roleText(mode, cfg)];
|
|
62
|
+
const parts = [`# Sub-Sub: ${mode === "librarian" ? "Librarian" : "Researcher"} mode`, roleText(mode, cfg)];
|
|
63
63
|
const shared = readIf(cfg.sharedRules);
|
|
64
64
|
if (shared) {
|
|
65
65
|
parts.push(
|
package/src/roles.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { profileSpec } from "./profiles.ts";
|
|
|
7
7
|
|
|
8
8
|
export const FILE_TOOLS = ["read", "edit", "write", "grep", "find", "ls"];
|
|
9
9
|
|
|
10
|
-
/** Prefixes of the tools that Sub-Sub's servers provide (the gate checks the mode for these). */
|
|
10
|
+
/** Prefixes of the tools that Sub-Sub's servers provide (the gate checks the mode and the profile for these). */
|
|
11
11
|
export const SERVER_PREFIXES = ["zotero_", "scholar_", "verify_"];
|
|
12
12
|
|
|
13
13
|
export const ZOTERO_READ = [
|
|
@@ -48,27 +48,48 @@ export const PREVIEW_WRITES = new Set([
|
|
|
48
48
|
/** Writes without a dry run that still need a yes. */
|
|
49
49
|
export const CONFIRM_WRITES = new Set(["scholar_export_bibliography"]);
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
/** Zotero tools that look at the library or write only a note in the Sub-Sub folder. */
|
|
52
|
+
export const ZOTERO_CHECKS = ["tag_audit", "audit_metadata", "find_duplicates", "check_retractions", "missing_pdfs", "write_tag_review"].map(
|
|
53
|
+
(n) => `zotero_${n}`,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
export interface ToolOptions {
|
|
58
|
+
profile?: Profile;
|
|
59
|
+
/** The model test: also the bakeoff_ tools. */
|
|
60
|
+
bench?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The tools of a mode. The Librarian manages the library: Zotero tools and files.
|
|
65
|
+
* The Researcher has the same, plus searches, notes and reference checks. The
|
|
66
|
+
* profile then removes tools. Every library change is still previewed (gateKind).
|
|
67
|
+
*/
|
|
68
|
+
export function toolsFor(mode: Mode, registered: string[], opts: ToolOptions = {}): string[] {
|
|
52
69
|
const has = new Set(registered);
|
|
53
|
-
const without = new Set(profileSpec(profile).without);
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
const without = new Set(profileSpec(opts.profile ?? "editor").without);
|
|
71
|
+
const zotero = registered.filter((n) => n.startsWith("zotero_") && (opts.bench || !n.startsWith("zotero_bakeoff_")));
|
|
72
|
+
const wanted = [
|
|
73
|
+
...ZOTERO_READ,
|
|
74
|
+
...ZOTERO_CHECKS,
|
|
75
|
+
...zotero,
|
|
76
|
+
...(mode === "researcher" ? registered.filter((n) => n.startsWith("scholar_") || n.startsWith("verify_")) : []),
|
|
77
|
+
...FILE_TOOLS,
|
|
78
|
+
];
|
|
62
79
|
return [...new Set(wanted)].filter((n) => has.has(n) && !without.has(n));
|
|
63
80
|
}
|
|
64
81
|
|
|
65
82
|
/** Why a Sub-Sub tool is not available now, or undefined when it is. */
|
|
66
|
-
export function unavailableReason(name: string, mode: Mode, registered: string[],
|
|
67
|
-
if (toolsFor(mode, registered,
|
|
83
|
+
export function unavailableReason(name: string, mode: Mode, registered: string[], opts: ToolOptions = {}): string | undefined {
|
|
84
|
+
if (toolsFor(mode, registered, opts).includes(name)) return undefined;
|
|
85
|
+
const profile = opts.profile ?? "editor";
|
|
68
86
|
if (profileSpec(profile).without.includes(name)) {
|
|
69
87
|
return `${name} is not part of the ${profileSpec(profile).label} profile. The user can change the profile with /profile.`;
|
|
70
88
|
}
|
|
71
|
-
|
|
89
|
+
if (mode === "librarian" && toolsFor("researcher", registered, opts).includes(name)) {
|
|
90
|
+
return `${name} is part of the Researcher, which also does everything the Librarian does. The user can switch with /researcher.`;
|
|
91
|
+
}
|
|
92
|
+
return `${name} is not available.`;
|
|
72
93
|
}
|
|
73
94
|
|
|
74
95
|
export type GateKind = "preview" | "confirm" | "path" | null;
|
|
@@ -84,7 +105,3 @@ export function gateKind(toolName: string, input: Record<string, unknown>): Gate
|
|
|
84
105
|
if (toolName === "write" || toolName === "edit") return "path";
|
|
85
106
|
return null;
|
|
86
107
|
}
|
|
87
|
-
|
|
88
|
-
export function otherMode(mode: Mode): Mode {
|
|
89
|
-
return mode === "librarian" ? "researcher" : "librarian";
|
|
90
|
-
}
|
package/src/subsub.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sub-Sub: a Zotero librarian and research assistant for pi.
|
|
3
3
|
*
|
|
4
|
-
* - Two modes
|
|
5
|
-
*
|
|
4
|
+
* - Two modes: the Librarian manages the library (Zotero tools only); the
|
|
5
|
+
* Researcher does the same and also searches PubMed, Europe PMC and OpenAlex
|
|
6
|
+
* and writes notes. Each mode can have its own model.
|
|
6
7
|
* - An approval gate: every library change is previewed by the server (dry run)
|
|
7
8
|
* and shown to the user; nothing is applied without a yes.
|
|
8
9
|
* - Profiles (reader, scholar, author, editor) set what Sub-Sub writes and which
|
|
9
10
|
* tools it offers; /profile changes it.
|
|
10
11
|
* - Commands: /librarian, /researcher, /profile, /subsub, /history, /undo.
|
|
11
12
|
* - Add-on: with "addons": ["starbuck"], Starbuck runs as a third server
|
|
12
|
-
* ("verify") for reference checks
|
|
13
|
+
* ("verify") for reference checks.
|
|
13
14
|
*
|
|
14
15
|
* The Python servers start in session_start (not in the factory) and stop in
|
|
15
16
|
* session_shutdown. Tools are registered once and use the current bridge.
|
|
@@ -25,9 +26,9 @@ import {
|
|
|
25
26
|
findUv,
|
|
26
27
|
isProfile,
|
|
27
28
|
loadConfig,
|
|
28
|
-
type Mode,
|
|
29
29
|
parseEnvFile,
|
|
30
30
|
type Profile,
|
|
31
|
+
type Mode,
|
|
31
32
|
saveConfig,
|
|
32
33
|
serverCommand,
|
|
33
34
|
starbuckCommand,
|
|
@@ -44,6 +45,8 @@ import { headerLines, type LibraryState, paintPreview, QUOTES, type Scheme, sche
|
|
|
44
45
|
import { systemAddition } from "./prompt.ts";
|
|
45
46
|
import { gateKind, SERVER_PREFIXES, toolsFor, unavailableReason } from "./roles.ts";
|
|
46
47
|
|
|
48
|
+
const MODE_ENTRY = "subsub-mode";
|
|
49
|
+
|
|
47
50
|
export interface SubsubDeps {
|
|
48
51
|
config?: SubsubConfig;
|
|
49
52
|
/** A ready bridge (tests). When given, Sub-Sub does not start or stop servers. */
|
|
@@ -52,7 +55,6 @@ export interface SubsubDeps {
|
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
const PACKAGE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
55
|
-
const MODE_ENTRY = "subsub-mode";
|
|
56
58
|
const BENCH_ENTRY = "subsub-bench";
|
|
57
59
|
|
|
58
60
|
function ownVersion(): string {
|
|
@@ -127,9 +129,11 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
127
129
|
|
|
128
130
|
if (deps.bridge) registerTools(deps.bridge);
|
|
129
131
|
|
|
130
|
-
pi.registerFlag("librarian", { description: "Start Sub-Sub in
|
|
132
|
+
pi.registerFlag("librarian", { description: "Start Sub-Sub in the Librarian mode", type: "boolean", default: false });
|
|
131
133
|
|
|
132
134
|
let mode: Mode = cfg.defaultMode;
|
|
135
|
+
const benchRun = Boolean(process.env.SUBSUB_BENCH_OUT);
|
|
136
|
+
const toolOpts = () => ({ profile, bench: benchRun });
|
|
133
137
|
let profile: Profile = cfg.profile;
|
|
134
138
|
const user = who(cfg);
|
|
135
139
|
/** True when started with the `subsub` command (not as a package inside plain pi). */
|
|
@@ -172,7 +176,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
172
176
|
function applyTheme(ctx: ExtensionContext): void {
|
|
173
177
|
if (!lookOn(ctx) || cfg.themes === false) return;
|
|
174
178
|
scheme ??= schemeFromAnsi(ctx.ui.theme.getFgAnsi("text"), ctx.ui.theme.name);
|
|
175
|
-
const name = themeName(
|
|
179
|
+
const name = themeName(scheme, cfg.themes || undefined);
|
|
176
180
|
const theme = name ? ctx.ui.getTheme(name) : undefined;
|
|
177
181
|
// A Theme object (not a name) changes the theme for this run only; the saved setting stays.
|
|
178
182
|
if (theme) ctx.ui.setTheme(theme);
|
|
@@ -250,7 +254,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
250
254
|
|
|
251
255
|
async function applyMode(ctx: ExtensionContext, next: Mode, remember: boolean): Promise<void> {
|
|
252
256
|
mode = next;
|
|
253
|
-
pi.setActiveTools(toolsFor(mode, registered(),
|
|
257
|
+
pi.setActiveTools(toolsFor(mode, registered(), toolOpts()));
|
|
254
258
|
const spec = cfg.models[mode];
|
|
255
259
|
if (spec) {
|
|
256
260
|
const i = spec.indexOf("/");
|
|
@@ -259,7 +263,6 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
259
263
|
if (!ok) ctx.ui.notify(`Sub-Sub: cannot use ${spec} (unknown model or no key); keeping the current model.`, "warning");
|
|
260
264
|
}
|
|
261
265
|
if (remember) pi.appendEntry(MODE_ENTRY, { mode });
|
|
262
|
-
applyTheme(ctx);
|
|
263
266
|
status(ctx);
|
|
264
267
|
}
|
|
265
268
|
|
|
@@ -277,6 +280,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
277
280
|
}
|
|
278
281
|
// Judge light or dark before Sub-Sub changes the theme.
|
|
279
282
|
if (lookOn(ctx)) scheme ??= schemeFromAnsi(ctx.ui.theme.getFgAnsi("text"), ctx.ui.theme.name);
|
|
283
|
+
applyTheme(ctx);
|
|
280
284
|
await applyMode(ctx, restored ?? (pi.getFlag("librarian") ? "librarian" : cfg.defaultMode), false);
|
|
281
285
|
if (lookOn(ctx)) {
|
|
282
286
|
setHeader(ctx);
|
|
@@ -323,7 +327,7 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
323
327
|
if (schema) coerceArgs(schema, input);
|
|
324
328
|
resolvePathArgs(name, input, (p) => normalizeToolPath(p, ctx.cwd));
|
|
325
329
|
if (SERVER_PREFIXES.some((p) => name.startsWith(p))) {
|
|
326
|
-
const why = unavailableReason(name, mode, registered(),
|
|
330
|
+
const why = unavailableReason(name, mode, registered(), toolOpts());
|
|
327
331
|
if (why) {
|
|
328
332
|
if (benchOut) record({ kind: "wrong_mode", tool: name, input });
|
|
329
333
|
return { block: true, reason: why };
|
|
@@ -438,18 +442,18 @@ export async function createSubsub(pi: ExtensionAPI, deps: SubsubDeps = {}): Pro
|
|
|
438
442
|
}
|
|
439
443
|
|
|
440
444
|
pi.registerCommand("librarian", {
|
|
441
|
-
description: "Sub-Sub: switch to the
|
|
445
|
+
description: "Sub-Sub: switch to the Librarian (manages the library: tags, imports, metadata, PDFs)",
|
|
442
446
|
handler: async (_args, ctx) => {
|
|
443
447
|
await applyMode(ctx, "librarian", true);
|
|
444
|
-
ctx.ui.notify("Sub-Sub:
|
|
448
|
+
ctx.ui.notify("Sub-Sub: Librarian mode. For searches and notes, switch to /researcher.", "info");
|
|
445
449
|
},
|
|
446
450
|
});
|
|
447
451
|
|
|
448
452
|
pi.registerCommand("researcher", {
|
|
449
|
-
description: "Sub-Sub: switch to the
|
|
453
|
+
description: "Sub-Sub: switch to the Researcher (searches, notes, reviews, and everything the Librarian does)",
|
|
450
454
|
handler: async (_args, ctx) => {
|
|
451
455
|
await applyMode(ctx, "researcher", true);
|
|
452
|
-
ctx.ui.notify("Sub-Sub:
|
|
456
|
+
ctx.ui.notify("Sub-Sub: Researcher mode. It searches, writes notes and also changes the library, with a preview.", "info");
|
|
453
457
|
},
|
|
454
458
|
});
|
|
455
459
|
|
package/src/web.ts
CHANGED
|
@@ -568,12 +568,11 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
568
568
|
if (busy) return send(res, 409, { error: "busy" });
|
|
569
569
|
const body = await readJson(req);
|
|
570
570
|
const spec = `${String(body.provider)}/${String(body.id)}`;
|
|
571
|
-
//
|
|
572
|
-
// extension reads the new setting.
|
|
571
|
+
// Remember the model for the current mode (or both), then restart pi so the extension reads it.
|
|
573
572
|
const mode = (subsubState as { mode?: string } | null)?.mode === "librarian" ? "librarian" : "researcher";
|
|
574
|
-
const models = { ...(loadConfig(env).models ?? {}), [mode]: spec };
|
|
573
|
+
const models: Record<string, string> = { ...(loadConfig(env).models ?? {}), [mode]: spec };
|
|
575
574
|
if (body.both) models[mode === "librarian" ? "researcher" : "librarian"] = spec;
|
|
576
|
-
saveConfig({ models }, env);
|
|
575
|
+
saveConfig({ models, model: undefined }, env);
|
|
577
576
|
await agent.request({ type: "set_model", provider: String(body.provider), modelId: String(body.id) });
|
|
578
577
|
await restartAgent();
|
|
579
578
|
return send(res, 200, { ok: true });
|
|
@@ -597,7 +596,7 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
597
596
|
const body = await readJson(req);
|
|
598
597
|
saveApiKey(agentDir, String(body.provider ?? ""), String(body.key ?? ""));
|
|
599
598
|
const current = loadConfig(env);
|
|
600
|
-
if (body.provider === "opencode-go" && Object.keys(current.models
|
|
599
|
+
if (body.provider === "opencode-go" && Object.keys(current.models).length === 0) saveConfig({ models: TESTED_MODELS, model: undefined }, env);
|
|
601
600
|
await restartAgent();
|
|
602
601
|
return send(res, 200, { ok: true });
|
|
603
602
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
Shared rules for
|
|
1
|
+
Shared rules for Sub-Sub. Sub-Sub reads this file at the start of every session. Edit it to change how notes look or where they go. Tag list: [[Zotero tags]].
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## What Sub-Sub does
|
|
4
4
|
|
|
5
|
-
- The
|
|
6
|
-
-
|
|
7
|
-
-
|
|
5
|
+
- Sub-Sub has two modes. The Librarian manages the library: tags, imports, metadata, PDFs, collections, notes in Zotero. The Researcher reads the library, searches PubMed, Europe PMC and OpenAlex, writes notes in this folder, and also does everything the Librarian does.
|
|
6
|
+
- Every library change shows a preview and needs the user's yes.
|
|
7
|
+
- Works found in a search are imported directly when the user chooses them. For a list to choose from later, Sub-Sub writes Inbox/Zotero import queue.md; the user ticks the lines and Sub-Sub imports them.
|
|
8
8
|
|
|
9
9
|
## Safety
|
|
10
10
|
|
|
@@ -85,5 +85,5 @@ created: YYYY-MM-DD
|
|
|
85
85
|
- Structure: scope and question; synthesis by theme; disagreements; gaps; a table of included items (citekey, year, design, main finding, source: full text or abstract).
|
|
86
86
|
- For a tag ("what does my library say about X"), include every item with the tag, one table row each. If an item is left out, say which and why.
|
|
87
87
|
- Cite with [@citekey] after each claim. Every claim must come from an included item.
|
|
88
|
-
- Works that are not in the library go in a separate section "Not in the library", with DOI or PMID, and are proposed for
|
|
88
|
+
- Works that are not in the library go in a separate section "Not in the library", with DOI or PMID, and are proposed for import.
|
|
89
89
|
- Link existing literature notes as [[citekey]].
|
package/web/app.css
CHANGED
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
--warn: #d6a84e;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/* The mode sets the accent: blue for the
|
|
90
|
+
/* The mode sets the accent: blue for the Researcher, rust for the Librarian. */
|
|
91
91
|
body { --accent: var(--researcher); --accent-soft: var(--researcher-soft); }
|
|
92
92
|
body[data-mode="librarian"] { --accent: var(--librarian); --accent-soft: var(--librarian-soft); }
|
|
93
93
|
|
|
@@ -316,7 +316,8 @@ select:hover { border-color: var(--rule-strong); }
|
|
|
316
316
|
border-radius: 5px;
|
|
317
317
|
}
|
|
318
318
|
.actions button:hover { background: var(--bg); }
|
|
319
|
-
.actions
|
|
319
|
+
.actions .group { margin: 0.55rem 0 0.15rem; padding: 0 0.5rem; font-size: 0.74rem; font-weight: 600; color: var(--muted); }
|
|
320
|
+
.actions .group:first-child { margin-top: 0; }
|
|
320
321
|
|
|
321
322
|
.side .grow { flex: 1; display: flex; flex-direction: column; min-height: 8rem; }
|
|
322
323
|
.side-subhead { display: flex; justify-content: space-between; align-items: baseline; padding-right: 0.3rem; }
|
package/web/app.js
CHANGED
|
@@ -175,7 +175,10 @@ function mode() {
|
|
|
175
175
|
|
|
176
176
|
function renderHeader() {
|
|
177
177
|
document.body.dataset.mode = mode();
|
|
178
|
-
for (const b of document.querySelectorAll(".modes button"))
|
|
178
|
+
for (const b of document.querySelectorAll(".modes button")) {
|
|
179
|
+
b.setAttribute("aria-pressed", String(b.dataset.mode === mode()));
|
|
180
|
+
b.title = t(b.dataset.mode === "librarian" ? "librarianHint" : "researcherHint");
|
|
181
|
+
}
|
|
179
182
|
if (state?.profile) $("profile").value = $("profile-side").value = state.profile;
|
|
180
183
|
$("starbuck-toggle").checked = Boolean(state?.starbuck);
|
|
181
184
|
$("starbuck-label").title = t("starbuckHint");
|
|
@@ -221,12 +224,18 @@ function hasCommand(name) {
|
|
|
221
224
|
function renderActions() {
|
|
222
225
|
const box = $("actions");
|
|
223
226
|
box.replaceChildren();
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
const shown = (a) =>
|
|
228
|
+
hasCommand(a.cmd) &&
|
|
229
|
+
|
|
230
|
+
(!a.needs || state?.[a.needs]) &&
|
|
231
|
+
(!a.profiles || a.profiles.includes(state?.profile ?? "scholar"));
|
|
232
|
+
const groups = mode() === "librarian" ? [["library", "groupLibrary"]] : [["research", "groupResearch"], ["library", "groupLibrary"]];
|
|
233
|
+
for (const [group, label] of groups) {
|
|
234
|
+
const list = ACTIONS[group].filter(shown);
|
|
235
|
+
if (!list.length) continue;
|
|
236
|
+
box.append(el("p", { class: "group", text: t(label) }));
|
|
237
|
+
for (const a of list) box.append(actionButton(a));
|
|
238
|
+
}
|
|
230
239
|
}
|
|
231
240
|
|
|
232
241
|
function actionButton(a) {
|
|
@@ -326,17 +335,17 @@ function exportConversation() {
|
|
|
326
335
|
if (text && text !== "/subsub-refresh") msgs.push(`### User\n\n${text}\n`);
|
|
327
336
|
} else if (m.classList.contains("assistant")) {
|
|
328
337
|
const text = Array.from(m.querySelectorAll(".md")).map((d) => d.innerText).join("\n\n");
|
|
329
|
-
if (text) msgs.push(`### Sub-Sub
|
|
338
|
+
if (text) msgs.push(`### Sub-Sub\n\n${text}\n`);
|
|
330
339
|
}
|
|
331
340
|
}
|
|
332
341
|
if (!msgs.length) return;
|
|
333
342
|
const date = new Date().toISOString().slice(0, 10);
|
|
334
|
-
const content = `# Sub-Sub Research Session (${date})\n\n**
|
|
343
|
+
const content = `# Sub-Sub Research Session (${date})\n\n**Profile**: ${state?.profile ?? "scholar"} \n\n---\n\n${msgs.join("\n---\n\n")}`;
|
|
335
344
|
const blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
|
|
336
345
|
const url = URL.createObjectURL(blob);
|
|
337
346
|
const a = document.createElement("a");
|
|
338
347
|
a.href = url;
|
|
339
|
-
a.download = `Sub-Sub-${
|
|
348
|
+
a.download = `Sub-Sub-${date}.md`;
|
|
340
349
|
document.body.append(a);
|
|
341
350
|
a.click();
|
|
342
351
|
a.remove();
|
|
@@ -369,7 +378,7 @@ function clearEmpty() {
|
|
|
369
378
|
|
|
370
379
|
function renderEmpty() {
|
|
371
380
|
if (log.children.length) return;
|
|
372
|
-
const s = SUGGESTIONS[mode()][lang] ??
|
|
381
|
+
const s = SUGGESTIONS[mode()].map((x) => x[lang] ?? x.en);
|
|
373
382
|
log.append(
|
|
374
383
|
el(
|
|
375
384
|
"div",
|
|
@@ -708,7 +717,8 @@ function onEvent(ev) {
|
|
|
708
717
|
break;
|
|
709
718
|
case "extension_ui_request":
|
|
710
719
|
if (ev.method === "notify") {
|
|
711
|
-
|
|
720
|
+
// The mode buttons show the mode; these confirmations would only repeat it.
|
|
721
|
+
if (/^Sub-Sub: (Librarian|Researcher) mode\./.test(ev.message)) break;
|
|
712
722
|
note(ev.message, ev.notifyType === "error" ? "error" : ev.notifyType === "warning" ? "warning" : "info", /\n/.test(ev.message));
|
|
713
723
|
} else {
|
|
714
724
|
dialogs.push(ev);
|
|
@@ -935,15 +945,17 @@ function autosize() {
|
|
|
935
945
|
|
|
936
946
|
async function sendMessage(text) {
|
|
937
947
|
const message = text.trim();
|
|
938
|
-
if (!message) return;
|
|
948
|
+
if (!message) return false;
|
|
939
949
|
const name = message.startsWith("/") ? message.slice(1).split(/\s/)[0] : "";
|
|
940
950
|
const isTemplate = name && commands.some((c) => c.name === name && c.source === "prompt");
|
|
941
951
|
if (isTemplate) sentTemplates.push(message);
|
|
942
952
|
try {
|
|
943
953
|
await api("/api/prompt", { message });
|
|
954
|
+
return true;
|
|
944
955
|
} catch (err) {
|
|
945
956
|
if (isTemplate) sentTemplates.pop();
|
|
946
957
|
note(err.message, "error");
|
|
958
|
+
return false;
|
|
947
959
|
}
|
|
948
960
|
}
|
|
949
961
|
|
package/web/i18n.js
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
export const STRINGS = {
|
|
5
5
|
en: {
|
|
6
|
-
researcher: "Researcher",
|
|
7
|
-
librarian: "Librarian",
|
|
8
6
|
profile: "Profile",
|
|
9
7
|
model: "Model",
|
|
10
8
|
noModel: "No model",
|
|
@@ -14,8 +12,6 @@ export const STRINGS = {
|
|
|
14
12
|
noConversations: "No earlier conversations.",
|
|
15
13
|
send: "Send",
|
|
16
14
|
stop: "Stop",
|
|
17
|
-
placeholderResearcher: "Ask about your library, or type / for commands",
|
|
18
|
-
placeholderLibrarian: "Ask for a change to your library, or type / for commands",
|
|
19
15
|
yes: "Yes",
|
|
20
16
|
no: "No",
|
|
21
17
|
ok: "OK",
|
|
@@ -44,7 +40,6 @@ export const STRINGS = {
|
|
|
44
40
|
connect: "Connect",
|
|
45
41
|
modelTitle: "Model",
|
|
46
42
|
chooseModel: "Choose a model",
|
|
47
|
-
forBoth: "Use it in both modes",
|
|
48
43
|
noModels: "No models yet. Connect a provider below.",
|
|
49
44
|
connectProvider: "Connect a provider",
|
|
50
45
|
provider: "Provider",
|
|
@@ -83,13 +78,20 @@ export const STRINGS = {
|
|
|
83
78
|
starbuckOn: "Reference checks are on. Use /verify, or ask the researcher to check a manuscript's references.",
|
|
84
79
|
starbuckOff: "Reference checks are off.",
|
|
85
80
|
composerHint: "Enter sends. Shift+Enter starts a new line. / shows the commands.",
|
|
86
|
-
emptyResearcher: "The researcher reads your library and searches PubMed, Europe PMC and OpenAlex. It cannot change your library.",
|
|
87
|
-
emptyLibrarian: "The librarian changes your library. You see a preview of each change, and nothing changes until you approve it.",
|
|
88
81
|
previewKicker: "Preview",
|
|
82
|
+
researcher: "Researcher",
|
|
83
|
+
librarian: "Librarian",
|
|
84
|
+
researcherHint: "Searches, reads and writes notes, and also changes the library (every change is previewed).",
|
|
85
|
+
librarianHint: "Manages the library only: tags, imports, metadata, PDFs. No literature search.",
|
|
86
|
+
placeholderResearcher: "Ask about your library or the literature, or type / for commands",
|
|
87
|
+
placeholderLibrarian: "Ask for a change to your library, or type / for commands",
|
|
88
|
+
emptyResearcher: "The Researcher reads your library, searches PubMed, Europe PMC and OpenAlex and writes notes. It also imports, tags and repairs items: every change shows a preview, and nothing changes until you approve it.",
|
|
89
|
+
emptyLibrarian: "The Librarian manages your library: tags, imports, metadata and PDFs. Every change shows a preview, and nothing changes until you approve it. For searches and notes, switch to the Researcher.",
|
|
90
|
+
forBoth: "Use it in both modes",
|
|
91
|
+
groupResearch: "Research",
|
|
92
|
+
groupLibrary: "Library",
|
|
89
93
|
},
|
|
90
94
|
pt: {
|
|
91
|
-
researcher: "Investigador",
|
|
92
|
-
librarian: "Bibliotecário",
|
|
93
95
|
profile: "Perfil",
|
|
94
96
|
model: "Modelo",
|
|
95
97
|
noModel: "Sem modelo",
|
|
@@ -99,8 +101,6 @@ export const STRINGS = {
|
|
|
99
101
|
noConversations: "Ainda não há conversas anteriores.",
|
|
100
102
|
send: "Enviar",
|
|
101
103
|
stop: "Parar",
|
|
102
|
-
placeholderResearcher: "Pergunte sobre a sua biblioteca, ou escreva / para ver os comandos",
|
|
103
|
-
placeholderLibrarian: "Peça uma alteração à biblioteca, ou escreva / para ver os comandos",
|
|
104
104
|
yes: "Sim",
|
|
105
105
|
no: "Não",
|
|
106
106
|
ok: "OK",
|
|
@@ -129,7 +129,6 @@ export const STRINGS = {
|
|
|
129
129
|
connect: "Ligar",
|
|
130
130
|
modelTitle: "Modelo",
|
|
131
131
|
chooseModel: "Escolha um modelo",
|
|
132
|
-
forBoth: "Usar nos dois modos",
|
|
133
132
|
noModels: "Ainda não há modelos. Ligue um fornecedor abaixo.",
|
|
134
133
|
connectProvider: "Ligar um fornecedor",
|
|
135
134
|
provider: "Fornecedor",
|
|
@@ -168,16 +167,27 @@ export const STRINGS = {
|
|
|
168
167
|
starbuckOn: "A verificação de referências está ligada. Use /verify, ou peça ao investigador que verifique as referências de um manuscrito.",
|
|
169
168
|
starbuckOff: "A verificação de referências está desligada.",
|
|
170
169
|
composerHint: "Enter envia. Shift+Enter muda de linha. / mostra os comandos.",
|
|
171
|
-
emptyResearcher: "O investigador lê a sua biblioteca e pesquisa na PubMed, Europe PMC e OpenAlex. Não pode alterar a biblioteca.",
|
|
172
|
-
emptyLibrarian: "O bibliotecário altera a sua biblioteca. Vê uma pré-visualização de cada alteração, e nada muda até a aprovar.",
|
|
173
170
|
previewKicker: "Pré-visualização",
|
|
171
|
+
researcher: "Investigador",
|
|
172
|
+
librarian: "Bibliotecário",
|
|
173
|
+
researcherHint: "Pesquisa, lê e escreve notas, e também altera a biblioteca (cada alteração é pré-visualizada).",
|
|
174
|
+
librarianHint: "Só gere a biblioteca: etiquetas, importações, metadados, PDF. Não pesquisa a literatura.",
|
|
175
|
+
placeholderResearcher: "Pergunte sobre a sua biblioteca ou a literatura, ou escreva / para ver os comandos",
|
|
176
|
+
placeholderLibrarian: "Peça uma alteração à biblioteca, ou escreva / para ver os comandos",
|
|
177
|
+
emptyResearcher: "O Investigador lê a sua biblioteca, pesquisa na PubMed, Europe PMC e OpenAlex e escreve notas. Também importa, etiqueta e corrige itens: cada alteração mostra uma pré-visualização, e nada muda até a aprovar.",
|
|
178
|
+
emptyLibrarian: "O Bibliotecário gere a sua biblioteca: etiquetas, importações, metadados e PDF. Cada alteração mostra uma pré-visualização, e nada muda até a aprovar. Para pesquisar e escrever notas, mude para o Investigador.",
|
|
179
|
+
forBoth: "Usar nos dois modos",
|
|
180
|
+
groupResearch: "Investigação",
|
|
181
|
+
groupLibrary: "Biblioteca",
|
|
174
182
|
},
|
|
175
183
|
};
|
|
176
184
|
|
|
177
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Buttons in the side panel, in two groups. `fill` puts the command in the message box; `send` runs it at once.
|
|
187
|
+
* The Librarian sees the Library group only. `profiles` and `needs` limit a button to some profiles or add-ons.
|
|
188
|
+
*/
|
|
178
189
|
export const ACTIONS = {
|
|
179
|
-
|
|
180
|
-
{ cmd: "subsub", send: true, en: "Library overview", pt: "Resumo da biblioteca" },
|
|
190
|
+
research: [
|
|
181
191
|
{ cmd: "lit", fill: true, profiles: ["scholar", "author", "editor"], en: "Literature review", pt: "Revisão da literatura" },
|
|
182
192
|
{ cmd: "compare", fill: true, profiles: ["scholar", "author", "editor"], en: "Compare sources", pt: "Comparar fontes" },
|
|
183
193
|
{ cmd: "lit-note", fill: true, en: "Literature note", pt: "Nota de leitura" },
|
|
@@ -190,28 +200,28 @@ export const ACTIONS = {
|
|
|
190
200
|
{ cmd: "request-copy", fill: true, en: "Ask an author for a copy", pt: "Pedir uma cópia ao autor" },
|
|
191
201
|
{ cmd: "digest", send: true, profiles: ["scholar", "author", "editor"], en: "Digest of the week", pt: "Resumo da semana" },
|
|
192
202
|
],
|
|
193
|
-
|
|
203
|
+
library: [
|
|
194
204
|
{ cmd: "subsub", send: true, en: "Library overview", pt: "Resumo da biblioteca" },
|
|
195
205
|
{ cmd: "tag-batch 10", send: true, en: "Tag 10 items", pt: "Etiquetar 10 itens" },
|
|
196
206
|
{ cmd: "clean-tags", send: true, en: "Clean up tags", pt: "Limpar etiquetas" },
|
|
197
207
|
{ cmd: "import-queue", send: true, en: "Import the queue", pt: "Importar a fila" },
|
|
198
|
-
],
|
|
199
|
-
both: [
|
|
200
208
|
{ cmd: "history", send: true, en: "Recent changes", pt: "Alterações recentes" },
|
|
201
209
|
{ cmd: "undo", send: true, en: "Undo the last change", pt: "Desfazer a última alteração" },
|
|
202
210
|
],
|
|
203
211
|
};
|
|
204
212
|
|
|
205
|
-
/** Suggestions on an empty conversation. */
|
|
213
|
+
/** Suggestions on an empty conversation, per mode. */
|
|
206
214
|
export const SUGGESTIONS = {
|
|
207
|
-
researcher:
|
|
208
|
-
en:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
pt:
|
|
214
|
-
|
|
215
|
+
researcher: [
|
|
216
|
+
{ en: "What do I have on ", pt: "O que tenho sobre " },
|
|
217
|
+
{ en: "Find recent systematic reviews on ", pt: "Procure revisões sistemáticas recentes sobre " },
|
|
218
|
+
{ en: "Which items have no PDF?", pt: "Que itens não têm PDF?" },
|
|
219
|
+
],
|
|
220
|
+
librarian: [
|
|
221
|
+
{ en: "Tag the next 10 items without a topic", pt: "Etiquete os próximos 10 itens sem tópico" },
|
|
222
|
+
{ en: "Import 10.1136/bmj.n71", pt: "Importe 10.1136/bmj.n71" },
|
|
223
|
+
{ en: "Find duplicates", pt: "Procure duplicados" },
|
|
224
|
+
],
|
|
215
225
|
};
|
|
216
226
|
|
|
217
227
|
/** Plain names for the tools, shown while they run. Unknown tools keep their name. */
|
package/web/index.html
CHANGED
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
<button type="button" class="link-btn" id="model-close" data-i18n="close">Close</button>
|
|
111
111
|
</div>
|
|
112
112
|
<h3 data-i18n="chooseModel">Choose a model</h3>
|
|
113
|
-
<label class="check"><input type="checkbox" id="model-both"
|
|
113
|
+
<label class="check"><input type="checkbox" id="model-both"> <span data-i18n="forBoth">Use it in both modes</span></label>
|
|
114
114
|
<ul class="models" id="models"></ul>
|
|
115
115
|
<h3 data-i18n="connectProvider">Connect a provider</h3>
|
|
116
116
|
<form id="login-form" class="login">
|