@tiagojct/subsub 0.11.0 → 0.11.2
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 +114 -179
- package/dist/bench.js +2 -2
- package/dist/bridge.js +5 -1
- package/dist/cli.js +34 -1
- package/dist/config.js +18 -2
- package/dist/doctor.js +9 -4
- package/dist/init.js +27 -6
- package/dist/paths.js +31 -4
- package/dist/preview.js +7 -2
- package/dist/prompt.js +3 -0
- package/dist/shortcut.js +2 -2
- package/dist/subsub.js +70 -8
- package/dist/web.js +73 -29
- package/package.json +2 -3
- package/prompts/lit-note.md +1 -1
- package/src/bench.ts +2 -2
- package/src/bridge.ts +4 -1
- package/src/cli.ts +34 -1
- package/src/config.ts +17 -2
- package/src/doctor.ts +8 -3
- package/src/init.ts +24 -5
- package/src/paths.ts +32 -3
- package/src/preview.ts +8 -2
- package/src/prompt.ts +6 -0
- package/src/shortcut.ts +2 -2
- package/src/subsub.ts +63 -12
- package/src/web.ts +67 -28
- package/web/app.css +1 -0
- package/web/app.js +42 -2
- package/web/i18n.js +11 -3
- package/docs/Subsub.md +0 -140
package/src/web.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { homedir } from "node:os";
|
|
|
22
22
|
import { extname, join, normalize, resolve, sep } from "node:path";
|
|
23
23
|
import { StringDecoder } from "node:string_decoder";
|
|
24
24
|
import { agentDirFor, chooseCwd, PACKAGE_DIR, packageVersion } from "./cli.ts";
|
|
25
|
-
import { expand, loadConfig, saveConfig, type SubsubConfig, withStarbuck } from "./config.ts";
|
|
25
|
+
import { configPath, expand, loadConfig, readConfigFile, saveConfig, type SubsubConfig, withStarbuck } from "./config.ts";
|
|
26
26
|
import { TESTED_MODELS } from "./init.ts";
|
|
27
27
|
import { obsidianRoot } from "./layout.ts";
|
|
28
28
|
import { realish, within } from "./paths.ts";
|
|
@@ -336,7 +336,7 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
336
336
|
env.PI_CODING_AGENT_DIR = agentDir; // for the session list below
|
|
337
337
|
|
|
338
338
|
const childEnv: NodeJS.ProcessEnv = { ...env, SUBSUB_WEB: "1" };
|
|
339
|
-
const baseArgs = [bin, "--mode", "rpc", "--here", ...
|
|
339
|
+
const baseArgs = [bin, "--mode", "rpc", "--here", ...opts.piArgs];
|
|
340
340
|
const agent = new Agent(process.execPath, baseArgs, cwd, childEnv);
|
|
341
341
|
|
|
342
342
|
// ---- state shared with the pages
|
|
@@ -346,8 +346,20 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
346
346
|
let busy = false;
|
|
347
347
|
let lastSeen = Date.now();
|
|
348
348
|
let restarting = false;
|
|
349
|
+
let switching = false; // a restart for a new model, add-on or key is under way
|
|
350
|
+
let lastSession: string | undefined;
|
|
349
351
|
let refreshAfterRun = false;
|
|
350
352
|
|
|
353
|
+
/** The mode to start pi in: the one on screen, else the --librarian flag. A session's own mode still wins. */
|
|
354
|
+
function modeArgs(): string[] {
|
|
355
|
+
const shown = (subsubState as { mode?: string } | null)?.mode;
|
|
356
|
+
if (shown === "librarian" || shown === "researcher") return [`--${shown}`];
|
|
357
|
+
return opts.librarian ? ["--librarian"] : [];
|
|
358
|
+
}
|
|
359
|
+
function startAgent(): void {
|
|
360
|
+
agent.start([...modeArgs(), ...(lastSession && existsSync(lastSession) ? ["--session", lastSession] : [])]);
|
|
361
|
+
}
|
|
362
|
+
|
|
351
363
|
function broadcast(ev: unknown): void {
|
|
352
364
|
const data = `data: ${JSON.stringify(ev)}\n\n`;
|
|
353
365
|
for (const c of clients) c.write(data);
|
|
@@ -384,12 +396,13 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
384
396
|
agent.onExit = (code) => {
|
|
385
397
|
if (restarting) return;
|
|
386
398
|
busy = false;
|
|
399
|
+
for (const id of dialogs.keys()) broadcast({ type: "dialog_closed", id });
|
|
387
400
|
dialogs.clear();
|
|
388
401
|
broadcast({ type: "agent_exit", code, stderr: agent.stderr.slice(-1500) });
|
|
389
402
|
log(`pi stopped (code ${code}).\n${agent.stderr.slice(-2000)}`);
|
|
390
403
|
};
|
|
391
404
|
|
|
392
|
-
|
|
405
|
+
startAgent();
|
|
393
406
|
|
|
394
407
|
async function snapshot(): Promise<Record<string, unknown>> {
|
|
395
408
|
const out: Record<string, unknown> = {
|
|
@@ -403,6 +416,7 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
403
416
|
};
|
|
404
417
|
try {
|
|
405
418
|
out.session = await agent.request({ type: "get_state" });
|
|
419
|
+
lastSession = (out.session as { sessionFile?: string }).sessionFile ?? lastSession;
|
|
406
420
|
out.messages = ((await agent.request({ type: "get_messages" })) as { messages: unknown[] }).messages;
|
|
407
421
|
out.commands = ((await agent.request({ type: "get_commands" })) as { commands: unknown[] }).commands;
|
|
408
422
|
} catch (err) {
|
|
@@ -422,22 +436,37 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
422
436
|
}
|
|
423
437
|
|
|
424
438
|
async function restartAgent(): Promise<void> {
|
|
425
|
-
let sessionFile: string | undefined;
|
|
426
439
|
try {
|
|
427
|
-
|
|
440
|
+
lastSession = ((await agent.request({ type: "get_state" })) as { sessionFile?: string }).sessionFile ?? lastSession;
|
|
428
441
|
} catch {
|
|
429
|
-
/*
|
|
442
|
+
/* keep the last one known */
|
|
430
443
|
}
|
|
431
444
|
restarting = true;
|
|
432
445
|
for (const id of dialogs.keys()) broadcast({ type: "dialog_closed", id });
|
|
433
446
|
dialogs.clear();
|
|
434
|
-
|
|
435
|
-
|
|
447
|
+
try {
|
|
448
|
+
await agent.stop();
|
|
449
|
+
} finally {
|
|
450
|
+
restarting = false;
|
|
451
|
+
}
|
|
436
452
|
busy = false;
|
|
437
|
-
|
|
453
|
+
startAgent();
|
|
438
454
|
broadcast(await snapshot());
|
|
439
455
|
}
|
|
440
456
|
|
|
457
|
+
/** Run a change that restarts pi, one at a time. */
|
|
458
|
+
async function withRestart(res: ServerResponse, change: () => Promise<void> | void): Promise<void> {
|
|
459
|
+
if (busy || switching) return send(res, 409, { error: "busy" });
|
|
460
|
+
switching = true;
|
|
461
|
+
try {
|
|
462
|
+
await change();
|
|
463
|
+
await restartAgent();
|
|
464
|
+
} finally {
|
|
465
|
+
switching = false;
|
|
466
|
+
}
|
|
467
|
+
return send(res, 200, { ok: true });
|
|
468
|
+
}
|
|
469
|
+
|
|
441
470
|
// ---- idle stop
|
|
442
471
|
let closing = false;
|
|
443
472
|
const idleTimer = setInterval(() => {
|
|
@@ -521,7 +550,13 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
521
550
|
const body = await readJson(req);
|
|
522
551
|
const message = String(body.message ?? "").trim();
|
|
523
552
|
if (!message) return send(res, 400, { error: "empty message" });
|
|
524
|
-
|
|
553
|
+
try {
|
|
554
|
+
await agent.request({ type: "prompt", message, ...(busy ? { streamingBehavior: "followUp" } : {}) });
|
|
555
|
+
} catch (err) {
|
|
556
|
+
// No provider yet: the page opens the model dialog instead of showing pi's text.
|
|
557
|
+
if (/no api key|no model|no models available/i.test((err as Error).message)) return send(res, 409, { error: "nomodel" });
|
|
558
|
+
throw err;
|
|
559
|
+
}
|
|
525
560
|
return send(res, 200, { ok: true });
|
|
526
561
|
}
|
|
527
562
|
case "POST /api/abort":
|
|
@@ -565,40 +600,44 @@ export async function startWeb(opts: WebOptions, env: NodeJS.ProcessEnv = proces
|
|
|
565
600
|
return send(res, 200, { models: Array.isArray(data) ? data : (data.models ?? []) });
|
|
566
601
|
}
|
|
567
602
|
case "POST /api/model": {
|
|
568
|
-
if (busy) return send(res, 409, { error: "busy" });
|
|
603
|
+
if (busy || switching) return send(res, 409, { error: "busy" });
|
|
569
604
|
const body = await readJson(req);
|
|
570
605
|
const spec = `${String(body.provider)}/${String(body.id)}`;
|
|
571
606
|
// Remember the model for the current mode (or both), then restart pi so the extension reads it.
|
|
572
607
|
const mode = (subsubState as { mode?: string } | null)?.mode === "librarian" ? "librarian" : "researcher";
|
|
573
|
-
|
|
608
|
+
// Start from what the file says, not from the built-in defaults, so a model the user never chose is not saved.
|
|
609
|
+
const raw = readConfigFile(configPath(env));
|
|
610
|
+
const saved = raw.models && typeof raw.models === "object" ? (raw.models as Record<string, string>) : typeof raw.model === "string" && raw.model ? { librarian: raw.model, researcher: raw.model } : {};
|
|
611
|
+
const models: Record<string, string> = { ...saved, [mode]: spec };
|
|
574
612
|
if (body.both) models[mode === "librarian" ? "researcher" : "librarian"] = spec;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
613
|
+
return withRestart(res, async () => {
|
|
614
|
+
saveConfig({ models, model: undefined }, env);
|
|
615
|
+
// The open conversation records the new model, so it keeps it after the restart.
|
|
616
|
+
await agent.request({ type: "set_model", provider: String(body.provider), modelId: String(body.id) }).catch(() => {});
|
|
617
|
+
});
|
|
579
618
|
}
|
|
580
619
|
case "POST /api/addons": {
|
|
581
620
|
// Turn an add-on on or off, then restart pi so the extension starts or stops its server.
|
|
582
|
-
if (busy) return send(res, 409, { error: "busy" });
|
|
621
|
+
if (busy || switching) return send(res, 409, { error: "busy" });
|
|
583
622
|
const body = await readJson(req);
|
|
584
623
|
if (typeof body.starbuck !== "boolean") return send(res, 400, { error: "starbuck must be true or false" });
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
624
|
+
return withRestart(res, () => {
|
|
625
|
+
saveConfig({ addons: withStarbuck(loadConfig(env).addons, body.starbuck) }, env);
|
|
626
|
+
});
|
|
588
627
|
}
|
|
589
628
|
case "POST /api/restart":
|
|
590
|
-
if (agent.proc) return send(res, 200, { ok: true });
|
|
591
|
-
|
|
629
|
+
if (agent.proc || switching) return send(res, 200, { ok: true });
|
|
630
|
+
startAgent();
|
|
592
631
|
broadcast(await snapshot());
|
|
593
632
|
return send(res, 200, { ok: true });
|
|
594
633
|
case "POST /api/login": {
|
|
595
|
-
if (busy) return send(res, 409, { error: "busy" });
|
|
634
|
+
if (busy || switching) return send(res, 409, { error: "busy" });
|
|
596
635
|
const body = await readJson(req);
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
636
|
+
return withRestart(res, () => {
|
|
637
|
+
saveApiKey(agentDir, String(body.provider ?? ""), String(body.key ?? ""));
|
|
638
|
+
const current = loadConfig(env);
|
|
639
|
+
if (body.provider === "opencode-go" && Object.keys(current.models).length === 0) saveConfig({ models: TESTED_MODELS, model: undefined }, env);
|
|
640
|
+
});
|
|
602
641
|
}
|
|
603
642
|
case "POST /api/open": {
|
|
604
643
|
const body = await readJson(req);
|
package/web/app.css
CHANGED
|
@@ -158,6 +158,7 @@ button:disabled { opacity: 0.45; cursor: default; }
|
|
|
158
158
|
.icon-btn:hover { color: var(--text); background: var(--surface); }
|
|
159
159
|
.icon-btn[aria-pressed="true"] { color: var(--accent); background: var(--accent-soft); }
|
|
160
160
|
|
|
161
|
+
.model-btn.needs { color: var(--warn); border-color: var(--warn); font-family: var(--sans); }
|
|
161
162
|
.chip-btn {
|
|
162
163
|
max-width: 16rem;
|
|
163
164
|
overflow: hidden;
|
package/web/app.js
CHANGED
|
@@ -183,7 +183,9 @@ function renderHeader() {
|
|
|
183
183
|
$("starbuck-toggle").checked = Boolean(state?.starbuck);
|
|
184
184
|
$("starbuck-label").title = t("starbuckHint");
|
|
185
185
|
const m = state?.model ?? session?.model;
|
|
186
|
-
|
|
186
|
+
const none = !m || !m.id || m.id === "unknown";
|
|
187
|
+
$("model-btn").textContent = $("model-btn-side").textContent = none ? t("noModel") : m.id;
|
|
188
|
+
for (const b of [$("model-btn"), $("model-btn-side")]) b.classList.toggle("needs", none);
|
|
187
189
|
const lib = $("library");
|
|
188
190
|
const txt = lib.querySelector(".library-text") ?? lib;
|
|
189
191
|
const L = state?.library;
|
|
@@ -650,6 +652,7 @@ function onEvent(ev) {
|
|
|
650
652
|
setBusy(!!ev.busy);
|
|
651
653
|
dialogs.length = 0;
|
|
652
654
|
for (const d of ev.dialogs ?? []) dialogs.push(d);
|
|
655
|
+
dropStaleDialog();
|
|
653
656
|
nextDialog();
|
|
654
657
|
loadSessions();
|
|
655
658
|
break;
|
|
@@ -743,6 +746,8 @@ function onEvent(ev) {
|
|
|
743
746
|
break;
|
|
744
747
|
case "agent_exit":
|
|
745
748
|
setBusy(false);
|
|
749
|
+
dialogs.length = 0;
|
|
750
|
+
dropStaleDialog();
|
|
746
751
|
log.append(
|
|
747
752
|
el(
|
|
748
753
|
"div",
|
|
@@ -776,6 +781,7 @@ function dialogTitle(d) {
|
|
|
776
781
|
if ((m = raw.match(/^apply (\S+)\?$/))) return t("apply", { x: toolLabel(m[1]) });
|
|
777
782
|
if ((m = raw.match(/^run (\S+)\?$/))) return t("run", { x: toolLabel(m[1]) });
|
|
778
783
|
if (raw === "undo?") return t("undo");
|
|
784
|
+
if ((m = raw.match(/^write the Starbuck report to (.+)\?$/))) return t("starbuckReport", { x: m[1] });
|
|
779
785
|
if ((m = raw.match(/^(write|edit) (.+)\?$/))) return t("write", { x: m[2] });
|
|
780
786
|
return raw;
|
|
781
787
|
}
|
|
@@ -797,6 +803,14 @@ function paintPreview(pre, text) {
|
|
|
797
803
|
}
|
|
798
804
|
}
|
|
799
805
|
|
|
806
|
+
/** Close the dialog on screen if Sub-Sub no longer waits for it (pi restarted, or another tab answered). */
|
|
807
|
+
function dropStaleDialog() {
|
|
808
|
+
if (shownDialog && !dialogs.some((d) => d.id === shownDialog.id)) {
|
|
809
|
+
shownDialog = null;
|
|
810
|
+
if ($("dialog").open) $("dialog").close();
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
800
814
|
function nextDialog() {
|
|
801
815
|
if (shownDialog || !dialogs.length) return;
|
|
802
816
|
const d = dialogs[0];
|
|
@@ -821,6 +835,7 @@ function nextDialog() {
|
|
|
821
835
|
note(err.message, "error");
|
|
822
836
|
}
|
|
823
837
|
};
|
|
838
|
+
$("dialog-form").onsubmit = (e) => e.preventDefault();
|
|
824
839
|
if (d.method === "confirm") {
|
|
825
840
|
buttons.append(
|
|
826
841
|
el("span", { class: "modal-shortcut-hint", text: t("confirmShortcutHint") }),
|
|
@@ -834,14 +849,26 @@ function nextDialog() {
|
|
|
834
849
|
const field = d.method === "editor" ? el("textarea", {}) : el("input", { type: "text", placeholder: d.placeholder ?? "" });
|
|
835
850
|
field.value = d.prefill ?? "";
|
|
836
851
|
body.append(field);
|
|
852
|
+
$("dialog-form").onsubmit = (e) => {
|
|
853
|
+
e.preventDefault();
|
|
854
|
+
answer({ value: field.value });
|
|
855
|
+
};
|
|
837
856
|
buttons.append(
|
|
838
857
|
el("button", { type: "button", class: "ghost", text: t("cancel"), onclick: () => answer({ cancelled: true }) }),
|
|
839
858
|
el("button", { type: "button", class: "primary", text: t("ok"), onclick: () => answer({ value: field.value }) }),
|
|
840
859
|
);
|
|
841
860
|
}
|
|
861
|
+
// Keys typed just before the dialog opened (the user was writing a message) must not answer it.
|
|
862
|
+
const openedAt = performance.now();
|
|
863
|
+
const yesKeys = lang === "pt" ? ["s", "S", "y", "Y"] : ["y", "Y"];
|
|
842
864
|
dlg.onkeydown = (e) => {
|
|
843
865
|
if (d.method === "confirm") {
|
|
844
|
-
if (e.
|
|
866
|
+
if (e.repeat || performance.now() - openedAt < 700) {
|
|
867
|
+
// Escape still declines (through oncancel); Enter and Space would press the focused button.
|
|
868
|
+
if (e.key !== "Escape" && e.key !== "Tab") e.preventDefault();
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (yesKeys.includes(e.key) || (e.key === "Enter" && (e.metaKey || e.ctrlKey))) {
|
|
845
872
|
e.preventDefault();
|
|
846
873
|
answer({ confirmed: true });
|
|
847
874
|
} else if (e.key === "n" || e.key === "N") {
|
|
@@ -867,6 +894,9 @@ function showModels() {
|
|
|
867
894
|
|
|
868
895
|
async function openModels() {
|
|
869
896
|
const dlg = $("model-dialog");
|
|
897
|
+
// Name the mode the choice is saved for; "both modes" is a choice for this time only.
|
|
898
|
+
dlg.querySelector("h2").textContent = t(mode() === "librarian" ? "modelTitleLibrarian" : "modelTitleResearcher");
|
|
899
|
+
$("model-both").checked = false;
|
|
870
900
|
const ul = $("models");
|
|
871
901
|
ul.replaceChildren(el("li", { class: "empty-models", text: "…" }));
|
|
872
902
|
const sel = $("login-provider");
|
|
@@ -954,6 +984,16 @@ async function sendMessage(text) {
|
|
|
954
984
|
return true;
|
|
955
985
|
} catch (err) {
|
|
956
986
|
if (isTemplate) sentTemplates.pop();
|
|
987
|
+
if (err.message === "nomodel") {
|
|
988
|
+
// Keep what the user wrote, and show where to connect a provider.
|
|
989
|
+
if (!input.value.trim()) {
|
|
990
|
+
input.value = message;
|
|
991
|
+
autosize();
|
|
992
|
+
}
|
|
993
|
+
note(t("noModelSend"), "warning");
|
|
994
|
+
showModels();
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
957
997
|
note(err.message, "error");
|
|
958
998
|
return false;
|
|
959
999
|
}
|
package/web/i18n.js
CHANGED
|
@@ -5,7 +5,8 @@ export const STRINGS = {
|
|
|
5
5
|
en: {
|
|
6
6
|
profile: "Profile",
|
|
7
7
|
model: "Model",
|
|
8
|
-
noModel: "
|
|
8
|
+
noModel: "Connect a model",
|
|
9
|
+
noModelSend: "No model is connected yet. Choose a provider and save its key; your message is still in the box.",
|
|
9
10
|
newConversation: "New conversation",
|
|
10
11
|
actions: "Actions",
|
|
11
12
|
conversations: "Conversations",
|
|
@@ -21,6 +22,7 @@ export const STRINGS = {
|
|
|
21
22
|
run: "Run {x}?",
|
|
22
23
|
undo: "Undo this change?",
|
|
23
24
|
write: "Allow writing to {x}?",
|
|
25
|
+
starbuckReport: "Write the Starbuck report to {x}?",
|
|
24
26
|
approveHint: "Nothing changes until you select Yes.",
|
|
25
27
|
zoteroChecking: "Zotero: checking",
|
|
26
28
|
zoteroDown: "Zotero is not running",
|
|
@@ -39,6 +41,8 @@ export const STRINGS = {
|
|
|
39
41
|
connectModel: "Connect a model to start.",
|
|
40
42
|
connect: "Connect",
|
|
41
43
|
modelTitle: "Model",
|
|
44
|
+
modelTitleResearcher: "Model for the Researcher",
|
|
45
|
+
modelTitleLibrarian: "Model for the Librarian",
|
|
42
46
|
chooseModel: "Choose a model",
|
|
43
47
|
noModels: "No models yet. Connect a provider below.",
|
|
44
48
|
connectProvider: "Connect a provider",
|
|
@@ -94,7 +98,8 @@ export const STRINGS = {
|
|
|
94
98
|
pt: {
|
|
95
99
|
profile: "Perfil",
|
|
96
100
|
model: "Modelo",
|
|
97
|
-
noModel: "
|
|
101
|
+
noModel: "Ligar um modelo",
|
|
102
|
+
noModelSend: "Ainda não há um modelo ligado. Escolha um fornecedor e guarde a chave; a sua mensagem continua na caixa.",
|
|
98
103
|
newConversation: "Nova conversa",
|
|
99
104
|
actions: "Ações",
|
|
100
105
|
conversations: "Conversas",
|
|
@@ -110,6 +115,7 @@ export const STRINGS = {
|
|
|
110
115
|
run: "Executar {x}?",
|
|
111
116
|
undo: "Desfazer esta alteração?",
|
|
112
117
|
write: "Permitir escrever em {x}?",
|
|
118
|
+
starbuckReport: "Escrever o relatório do Starbuck em {x}?",
|
|
113
119
|
approveHint: "Nada muda até selecionar Sim.",
|
|
114
120
|
zoteroChecking: "Zotero: a verificar",
|
|
115
121
|
zoteroDown: "O Zotero não está aberto",
|
|
@@ -128,6 +134,8 @@ export const STRINGS = {
|
|
|
128
134
|
connectModel: "Ligue um modelo para começar.",
|
|
129
135
|
connect: "Ligar",
|
|
130
136
|
modelTitle: "Modelo",
|
|
137
|
+
modelTitleResearcher: "Modelo do Investigador",
|
|
138
|
+
modelTitleLibrarian: "Modelo do Bibliotecário",
|
|
131
139
|
chooseModel: "Escolha um modelo",
|
|
132
140
|
noModels: "Ainda não há modelos. Ligue um fornecedor abaixo.",
|
|
133
141
|
connectProvider: "Ligar um fornecedor",
|
|
@@ -161,7 +169,7 @@ export const STRINGS = {
|
|
|
161
169
|
searchConversations: "Procurar conversas",
|
|
162
170
|
export: "Exportar",
|
|
163
171
|
exportTitle: "Exportar conversa",
|
|
164
|
-
confirmShortcutHint: "
|
|
172
|
+
confirmShortcutHint: "S para aprovar, N para recusar",
|
|
165
173
|
starbuck: "Verificação de referências (Starbuck)",
|
|
166
174
|
starbuckHint: "Verifica se as obras citadas existem, correspondem à citação e não foram retratadas. O Sub-Sub reinicia quando muda esta opção.",
|
|
167
175
|
starbuckOn: "A verificação de referências está ligada. Use /verify, ou peça ao investigador que verifique as referências de um manuscrito.",
|
package/docs/Subsub.md
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
Sub-Sub is a Zotero librarian and research assistant that runs in pi. It uses the Python servers of zotero-local-mcp for the library: previews, version checks, journal and undo, the tag vocabulary.
|
|
2
|
-
|
|
3
|
-
- Code: ~/Projects/subsub (git). Servers: ~/Projects/zotero-local-mcp
|
|
4
|
-
- Shared rules: [[Zotero agent]]. Tag vocabulary: [[Zotero tags]]
|
|
5
|
-
- Settings: ~/.config/subsub/config.json (written by `subsub init`) and ~/.config/zotero-local-mcp/env (also read by the weekly alerts in launchd)
|
|
6
|
-
- Sub-Sub folder: ~/Obsidian/Powerslave/Sub-Sub, with Inbox/, Literature/, Syntheses/, Research/ and Zotero/ (tag list, note formats, alert searches). Sub-Sub writes only there without asking.
|
|
7
|
-
- Models: Librarian opencode-go/mimo-v2.6-flash, Researcher opencode-go/mimo-v2.6-pro
|
|
8
|
-
- Public docs: the README in ~/Projects/subsub (source for subsub.tiagojacinto.eu)
|
|
9
|
-
|
|
10
|
-
## Safety
|
|
11
|
-
|
|
12
|
-
- Approval is enforced in code. Before any library change, Sub-Sub asks the server for the preview and shows it in a dialog. The change runs only after you select Yes. The model cannot skip this.
|
|
13
|
-
- The model does not need to do a dry run first. You see one dialog per change.
|
|
14
|
-
- Library tools run one at a time, so each preview includes the effect of earlier changes in the same step.
|
|
15
|
-
- The Librarian has no search tools. The Researcher has all tools; every library change still needs a Yes.
|
|
16
|
-
- bash is off. File writes outside the Sub-Sub folder, and writes to the rule files, config folders and code folders, need a Yes.
|
|
17
|
-
|
|
18
|
-
## Install
|
|
19
|
-
|
|
20
|
-
1. Make sure that Node is version 22.19 or later: type `node --version`.
|
|
21
|
-
2. Type `cd ~/Projects/subsub && npm install --omit=dev`.
|
|
22
|
-
3. Type `npm link`. This adds the `subsub` command.
|
|
23
|
-
4. Optional: type `pi remove ~/Projects/subsub`. Then plain `pi` stays a plain coding agent without Sub-Sub.
|
|
24
|
-
5. Type `subsub`. If pi has no OpenCode Go key yet, type `/login`, select OpenCode Zen and Go, and paste the key.
|
|
25
|
-
6. Type `/subsub`. Make sure that the result shows "Zotero: reachable" and the model for the mode (mimo-v2.6-pro in the Researcher).
|
|
26
|
-
7. Type `subsub init` once. Keep the Sub-Sub folder, the tag list, the email and the models. For the profile, select Editor.
|
|
27
|
-
8. Type `subsub doctor`. Make sure that no line shows FIX.
|
|
28
|
-
|
|
29
|
-
If you change the Node version with fnm, do step 3 again.
|
|
30
|
-
|
|
31
|
-
## Use
|
|
32
|
-
|
|
33
|
-
1. Start Zotero.
|
|
34
|
-
2. Type `subsub`. From the home folder, Sub-Sub starts in the Sub-Sub folder. From a different folder (for example a manuscript folder), it starts in that folder. To stay in the home folder, type `subsub --here`.
|
|
35
|
-
3. Sub-Sub starts in the Researcher. The Researcher reads, searches, writes notes and changes the library. The Librarian only manages the library. To start in the Librarian, type `subsub --librarian`.
|
|
36
|
-
4. To change the mode, type `/librarian` or `/researcher`. The mode also changes the model. After research, ask the Researcher to import the works you choose. It imports them in the same conversation, with one preview, and offers to tag them.
|
|
37
|
-
5. When a dialog shows a preview, read it. Select Yes to apply the change, or No to stop it. After No, tell Sub-Sub what to change.
|
|
38
|
-
6. To continue the last session, type `subsub -c`. To select an older session, type `subsub -r`.
|
|
39
|
-
|
|
40
|
-
Sub-Sub always replies in English and writes its notes in English. Quotations and titles of works stay in their original language.
|
|
41
|
-
|
|
42
|
-
Sub-Sub keeps its pi settings, sessions and packages in ~/.subsub/agent, separate from plain pi (~/.pi/agent).
|
|
43
|
-
|
|
44
|
-
Commands:
|
|
45
|
-
|
|
46
|
-
- `/subsub`: Zotero connection and library overview
|
|
47
|
-
- `/profile`: shows or changes the profile
|
|
48
|
-
- `/history`: recent library changes
|
|
49
|
-
- `/undo`: shows the last change, asks, and reverts it. `/undo <id>` reverts a specific change from `/history`.
|
|
50
|
-
|
|
51
|
-
Templates (type the name, then the argument):
|
|
52
|
-
|
|
53
|
-
- Library (both modes): `/tag-batch [size]`, `/clean-tags`, `/import-queue [note path]`
|
|
54
|
-
- Research (Researcher): `/lit-note <citekey>`, `/synthesis <topic>`, `/gaps <topic>`, `/manuscript <path>`, `/alert`
|
|
55
|
-
|
|
56
|
-
## Web view
|
|
57
|
-
|
|
58
|
-
1. Type `subsub web`. Sub-Sub opens in the browser, with the same profiles, tools and approvals. The mode buttons are at the top. The side panel shows the actions of the mode. The approval dialog shows the server preview; the change runs only after Yes.
|
|
59
|
-
2. To add a shortcut (Applications on macOS), type `subsub shortcut`. To remove it, type `subsub shortcut --remove`.
|
|
60
|
-
3. The server listens only on 127.0.0.1 and needs the random key in the address that `subsub web` opens. A second `subsub web` opens the running one.
|
|
61
|
-
4. The server stops 10 minutes after the last page closes. `subsub web --stay` keeps it running.
|
|
62
|
-
5. The labels are in European Portuguese when the language setting is Portuguese. A theme toggle button in the header switches between light, dark and system themes.
|
|
63
|
-
6. Faster review: in approval dialogs, press `y` to apply or `n` to cancel without using the mouse.
|
|
64
|
-
7. One-click copy buttons appear on every code block and assistant note. The sidebar includes real-time conversation search and an Export button to download the discussion as Markdown.
|
|
65
|
-
8. The model button lists the models and saves an API key for a provider (the same auth.json as `/login`). It saves the model for the current mode. To use the model in both modes, select "Use it in both modes".
|
|
66
|
-
|
|
67
|
-
The log is in ~/.subsub/web.log.
|
|
68
|
-
|
|
69
|
-
## Profiles
|
|
70
|
-
|
|
71
|
-
The profile sets how much Sub-Sub does. To see the profile, type `/profile`. To change it, type `/profile reader` (or scholar, author, editor). The change is saved.
|
|
72
|
-
|
|
73
|
-
- Reader: explains each step. Reading notes are quotes with page numbers, plus questions. No summaries or syntheses. No bulk library changes.
|
|
74
|
-
- Scholar: literature notes, syntheses, searches and alerts. All library changes.
|
|
75
|
-
- Author: Scholar plus manuscripts: citation check, bibliography, comments on the argument. It does not write the paragraphs.
|
|
76
|
-
- Editor: everything, including drafted manuscript text when you ask.
|
|
77
|
-
|
|
78
|
-
## Tag review notes
|
|
79
|
-
|
|
80
|
-
Sub-Sub writes proposed tags in review notes in Inbox/ ("Zotero tag review NN.md"). Edit the Proposed tags column. Write skip to leave an item as it is.
|
|
81
|
-
|
|
82
|
-
To apply one note, in either mode, type: apply the tag review Inbox/Zotero tag review NN.md.
|
|
83
|
-
|
|
84
|
-
To apply many notes:
|
|
85
|
-
|
|
86
|
-
1. Start Zotero.
|
|
87
|
-
2. Type `subsub review preview 13-31` (the numbers of the notes). This shows what each note will change. Nothing changes.
|
|
88
|
-
3. Type `subsub review apply 13-31`. Read the preview, then type y.
|
|
89
|
-
4. If Zotero asks for permission, select Always Allow.
|
|
90
|
-
|
|
91
|
-
Use note numbers, not a pattern such as "*.md": a pattern also takes older notes that have no Applied line, and applying an old note again replaces later tag changes.
|
|
92
|
-
|
|
93
|
-
Notes that were already applied are left out. Each applied note gets an "Applied" line with the journal ID. To revert a note, type `/undo <journal ID>` in Sub-Sub. If a note shows "changed since the note was written", the tags of those items changed in Zotero after the note was made; applying the note replaces those changes.
|
|
94
|
-
|
|
95
|
-
## Look
|
|
96
|
-
|
|
97
|
-
When you start Sub-Sub with the `subsub` command, it shows a banner, a status line (mode, model, Zotero items, items to review) and a line from Moby-Dick. The theme is Glauca, in the dark or light version that matches your terminal (set "themes": "subsub-try-works" for the other one). The theme changes for the session only; your pi theme setting stays.
|
|
98
|
-
|
|
99
|
-
To change this, add to ~/.config/subsub/config.json:
|
|
100
|
-
|
|
101
|
-
- `"quotes": false`: no Moby-Dick line.
|
|
102
|
-
- `"themes": false`: keep your own pi theme.
|
|
103
|
-
- `"look": false`: no banner and no themes.
|
|
104
|
-
|
|
105
|
-
The themes are in ~/Projects/subsub/themes. To make them again after a change in Gam, see scripts/make-themes.py.
|
|
106
|
-
|
|
107
|
-
## Model test
|
|
108
|
-
|
|
109
|
-
The model test runs several models on the same tasks against your library and compares them. Nothing in the library changes: each library change is recorded with its preview and then blocked. Notes go to ~/Projects/subsub/bench-results/<date>/, not to the Sub-Sub folder.
|
|
110
|
-
|
|
111
|
-
Tasks:
|
|
112
|
-
|
|
113
|
-
- Library tasks: tag 20 items against a blind reference (reference.md shows the items without their tags; the tags go in reference.json); fix the items without topic or status; import identifiers, one of them already in the library.
|
|
114
|
-
- Research tasks: a literature note on one item with full text; a synthesis on one topic; a search for recent papers that are not in the library.
|
|
115
|
-
|
|
116
|
-
Procedure:
|
|
117
|
-
|
|
118
|
-
1. Start Zotero.
|
|
119
|
-
2. Type `subsub-bench prepare`. This picks the tasks.
|
|
120
|
-
3. Type `caffeinate -i subsub-bench run`. This takes 1 to 2 hours. caffeinate keeps the Mac awake. If the run stops, type the same command again: finished runs are not repeated.
|
|
121
|
-
4. Type `subsub-bench score`. The result is results.md in the results folder.
|
|
122
|
-
|
|
123
|
-
Options for `run`: `--models a,b` for all tasks, or `--librarian a,b` and `--researcher c,d` for each group of tasks (OpenCode Go model ids), `--only tagging,import`, `--parallel 3`, `--timeout 900`. Library tasks run in the Librarian. Research tasks run in the Researcher.
|
|
124
|
-
|
|
125
|
-
Each model gets a code (for example M417), so the notes can be judged blind. key.json has the codes.
|
|
126
|
-
|
|
127
|
-
## Problems
|
|
128
|
-
|
|
129
|
-
- To find a setup problem, type `subsub doctor`. Each line marked FIX says what to do.
|
|
130
|
-
- "the zotero server did not start": run `cd ~/Projects/zotero-local-mcp && uv sync`. If pi does not find uv, add `"uvPath": "/path/to/uv"` to ~/.config/subsub/config.json (type `which uv` to see the path).
|
|
131
|
-
- "cannot use opencode-go/...": the OpenCode Go key is missing. In Sub-Sub, type `/login` again.
|
|
132
|
-
- "command not found: subsub": type `cd ~/Projects/subsub && npm link`.
|
|
133
|
-
- To update Sub-Sub after new code arrives, type `cd ~/Projects/subsub && npm install --omit=dev`. Do not use `subsub update`.
|
|
134
|
-
- A tool call is refused with "part of the Researcher": type `/researcher`.
|
|
135
|
-
- `subsub review` says "No note matches": check the note numbers in Inbox/. A path is written from the Sub-Sub folder, in quotes (for example "Inbox/Zotero tag review 13.md").
|
|
136
|
-
|
|
137
|
-
## Tests
|
|
138
|
-
|
|
139
|
-
- `cd ~/Projects/subsub && npm install && npm test`: unit tests.
|
|
140
|
-
- `npm run test:e2e`: real pi and the `subsub` command with a scripted model and a fake Zotero.
|